React - useCallback : Mémorisation de Fonctions

Quand tu passes des fonctions en props à des composants memo(), useCallback va mémoriser la fonction et éviter les re-renders inutiles.


Le Problème des Fonctions Recréées

Nouvelles Fonctions à Chaque Rendu

function ProblematicParent() {
  const [count, setCount] = useState(0)
  const [items, setItems] = useState([])
  const [name, setName] = useState('')

  // ❌ PROBLÈME - Nouvelle fonction à chaque rendu !
  const addItem = (text) => {
    setItems(prev => [...prev, { id: Date.now(), text }])
  }

  // ❌ PROBLÈME - Nouvelle fonction à chaque rendu !
  const handleClick = (id) => {
    console.log('Item clicked:', id)
  }

  // ❌ PROBLÈME - Nouvelle fonction à chaque rendu !
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Form submitted')
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      
      {/* Ces composants re-render même si leurs props logiques n'ont pas changé ! */}
      <ExpensiveForm onSubmit={handleSubmit} />
      <ExpensiveList items={items} onItemClick={handleClick} />
      <ExpensiveAddButton onAdd={addItem} />
    </div>
  )
}

// Ces composants optimisés ne servent à rien car les fonctions changent toujours !
const ExpensiveForm = memo(function ExpensiveForm({ onSubmit }) {
  console.log('📝 ExpensiveForm re-rendu') // ← Toujours appelé !
  return <form onSubmit={onSubmit}><button>Submit</button></form>
})

const ExpensiveList = memo(function ExpensiveList({ items, onItemClick }) {
  console.log('📝 ExpensiveList re-rendu') // ← Toujours appelé !
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.text}
        </li>
      ))}
    </ul>
  )
})

const ExpensiveAddButton = memo(function ExpensiveAddButton({ onAdd }) {
  console.log('🔘 ExpensiveAddButton re-rendu') // ← Toujours appelé !
  return <button onClick={() => onAdd('New item')}>Add Item</button>
})

useCallback

Syntaxe et Usage Basique

import { useCallback } from 'react'

function OptimizedParent() {
  const [count, setCount] = useState(0)
  const [items, setItems] = useState([])
  const [name, setName] = useState('')

  // ✅ SOLUTION - Fonction mémorisée stable
  const addItem = useCallback((text) => {
    setItems(prev => [...prev, { id: Date.now(), text, completed: false }])
  }, []) // ← Pas de dépendances = fonction stable

  // ✅ SOLUTION - Fonction mémorisée stable
  const handleClick = useCallback((id) => {
    console.log('Item clicked:', id)
    // Logique qui n'a pas de dépendances externes
  }, [])

  // ✅ SOLUTION - Fonction avec dépendances
  const addItemWithPrefix = useCallback((text) => {
    const prefix = name ? `[${name}] ` : ''
    setItems(prev => [...prev, { 
      id: Date.now(), 
      text: prefix + text,
      completed: false 
    }])
  }, [name]) // ← Se recrée seulement si name change

  // ✅ SOLUTION - Event handler optimisé
  const handleCountChange = useCallback((increment) => {
    setCount(prev => prev + increment)
  }, [])

  // ✅ SOLUTION - Fonction de suppression
  const removeItem = useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id))
  }, [])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1 (non-optimisé)</button>
      
      <input 
        value={name} 
        onChange={e => setName(e.target.value)} 
        placeholder="Préfixe..."
      />
      
      {/* Maintenant ces composants ne re-render que si nécessaire ! */}
      <OptimizedButton onClick={handleCountChange} increment={1}>+1</OptimizedButton>
      <OptimizedButton onClick={handleCountChange} increment={5}>+5</OptimizedButton>
      
      <OptimizedList 
        items={items} 
        onItemClick={handleClick}
        onItemRemove={removeItem}
      />
      
      <OptimizedAddForm 
        onAdd={addItem}
        onAddWithPrefix={addItemWithPrefix}
      />
    </div>
  )
}

// Maintenant ces composants ne re-render que si leurs props changent vraiment !
const OptimizedButton = memo(function OptimizedButton({ onClick, increment, children }) {
  console.log(`🔘 OptimizedButton (+${increment}) re-rendu`)
  
  return (
    <button onClick={() => onClick(increment)} style={{ margin: '5px' }}>
      {children}
    </button>
  )
})

const OptimizedList = memo(function OptimizedList({ items, onItemClick, onItemRemove }) {
  console.log('📝 OptimizedList re-rendu')
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} style={{ display: 'flex', justifyContent: 'space-between' }}>
          <span onClick={() => onItemClick(item.id)} style={{ cursor: 'pointer' }}>
            {item.text}
          </span>
          <button onClick={() => onItemRemove(item.id)}>❌</button>
        </li>
      ))}
    </ul>
  )
})

const OptimizedAddForm = memo(function OptimizedAddForm({ onAdd, onAddWithPrefix }) {
  console.log('📝 OptimizedAddForm re-rendu')
  const [input, setInput] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (input.trim()) {
      onAdd(input.trim())
      setInput('')
    }
  }

  const handleSubmitWithPrefix = (e) => {
    e.preventDefault()
    if (input.trim()) {
      onAddWithPrefix(input.trim())
      setInput('')
    }
  }

  return (
    <form>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="Nouvel élément..."
      />
      <button type="submit" onClick={handleSubmit}>Ajouter</button>
      <button type="button" onClick={handleSubmitWithPrefix}>Avec préfixe</button>
    </form>
  )
})

Ressources Pour Aller Plus Loin