React - Custom Hooks : Réutilisabilité

Les hooks personnalisés te permettent d'extraire la logique des composants et de la réutiliser partout. C'est l'un des trucs les plus puissants de React !
Attention ceci dit, c'est utile dans le cas ou on veux extraire la logique d'un endroit qui est utilisé à plusieurs endroits !


Qu'est-ce qu'un Custom Hook ?

Définition

Un custom hook est une fonction JavaScript qui :

  1. Commence par "use" (convention obligatoire)
  2. Peut utiliser d'autres hooks (useState, useEffect, etc.)
  3. Retourne de la logique réutilisable
  4. Suit les règles des hooks
// ✅ Custom Hook basique
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = useCallback(() => setCount(c => c + 1), [])
  const decrement = useCallback(() => setCount(c => c - 1), [])
  const reset = useCallback(() => setCount(initialValue), [initialValue])

  return { count, increment, decrement, reset }
}

// Utilisation
function Counter() {
  const { count, increment, decrement, reset } = useCounter(10)

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

Hooks Utilitaires Simples

useToggle - Basculer un Booléen

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue)

  const toggle = useCallback(() => setValue(v => !v), [])
  const setTrue = useCallback(() => setValue(true), [])
  const setFalse = useCallback(() => setValue(false), [])

  return [
    value,
    {
      toggle,
      setTrue,
      setFalse,
      setValue
    }
  ]
}

// Utilisation
function ToggleDemo() {
  const [isVisible, visibility] = useToggle(false)
  const [isLoading, loading] = useToggle(false)
  const [isDarkMode, darkMode] = useToggle(true)

  const simulateLoading = async () => {
    loading.setTrue()
    await new Promise(resolve => setTimeout(resolve, 2000))
    loading.setFalse()
  }

  return (
    <div style={{
      backgroundColor: isDarkMode ? '#333' : '#fff',
      color: isDarkMode ? '#fff' : '#333',
      padding: '20px',
      minHeight: '200px'
    }}>
      <h3>useToggle Demo</h3>
      
      <div>
        <button onClick={visibility.toggle}>
          {isVisible ? 'Masquer' : 'Afficher'} le contenu
        </button>
        {isVisible && <p>🎉 Contenu visible !</p>}
      </div>

      <div>
        <button onClick={simulateLoading} disabled={isLoading}>
          {isLoading ? 'Chargement...' : 'Simuler chargement'}
        </button>
      </div>

      <div>
        <button onClick={darkMode.toggle}>
          {isDarkMode ? '☀️ Mode clair' : '🌙 Mode sombre'}
        </button>
      </div>
    </div>
  )
}

useLocalStorage - Persistance Automatique

function useLocalStorage(key, initialValue) {
  // Récupérer la valeur initiale depuis localStorage
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(`Erreur localStorage pour "${key}":`, error)
      return initialValue
    }
  })

  // Fonction pour sauvegarder dans localStorage
  const setValue = useCallback((value) => {
    try {
      // Permettre à value d'être une fonction
      const valueToStore = value instanceof Function ? value(storedValue) : value
      
      setStoredValue(valueToStore)
      
      if (valueToStore === undefined) {
        window.localStorage.removeItem(key)
      } else {
        window.localStorage.setItem(key, JSON.stringify(valueToStore))
      }
    } catch (error) {
      console.error(`Erreur sauvegarde localStorage pour "${key}":`, error)
    }
  }, [key, storedValue])

  // Fonction pour supprimer
  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key)
      setStoredValue(initialValue)
    } catch (error) {
      console.error(`Erreur suppression localStorage pour "${key}":`, error)
    }
  }, [key, initialValue])

  return [storedValue, setValue, removeValue]
}

// Utilisation
function UserPreferences() {
  const [name, setName, removeName] = useLocalStorage('user-name', '')
  const [theme, setTheme] = useLocalStorage('user-theme', 'light')
  const [settings, setSettings] = useLocalStorage('user-settings', {
    notifications: true,
    language: 'fr'
  })

  const updateSetting = (key, value) => {
    setSettings(prev => ({ ...prev, [key]: value }))
  }

  return (
    <div>
      <h3>Préférences Utilisateur (persistantes)</h3>
      
      <div>
        <label>
          Nom:
          <input
            value={name}
            onChange={e => setName(e.target.value)}
            placeholder="Votre nom..."
          />
        </label>
        <button onClick={removeName}>Supprimer nom</button>
      </div>

      <div>
        <label>
          <input
            type="checkbox"
            checked={theme === 'dark'}
            onChange={e => setTheme(e.target.checked ? 'dark' : 'light')}
          />
          Mode sombre
        </label>
      </div>

      <div>
        <label>
          <input
            type="checkbox"
            checked={settings.notifications}
            onChange={e => updateSetting('notifications', e.target.checked)}
          />
          Notifications
        </label>
      </div>

      <div>
        <label>
          Langue:
          <select
            value={settings.language}
            onChange={e => updateSetting('language', e.target.value)}
          >
            <option value="fr">Français</option>
            <option value="en">English</option>
          </select>
        </label>
      </div>

      <div style={{ marginTop: '20px', fontSize: '12px', color: '#666' }}>
        <p>✅ Toutes les données sont sauvegardées automatiquement !</p>
        <p>Nom: {name || 'Non défini'}</p>
        <p>Thème: {theme}</p>
        <p>Paramètres: {JSON.stringify(settings)}</p>
      </div>
    </div>
  )
}

usePrevious - Valeur Précédente

function usePrevious(value) {
  const ref = useRef()
  
  useEffect(() => {
    ref.current = value
  })
  
  return ref.current
}

// Utilisation
function PreviousDemo() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')
  
  const previousCount = usePrevious(count)
  const previousName = usePrevious(name)

  return (
    <div>
      <h3>usePrevious Demo</h3>
      
      <div>
        <p>Count actuel: {count}</p>
        <p>Count précédent: {previousCount}</p>
        <p>Différence: {count - (previousCount || 0)}</p>
        <button onClick={() => setCount(c => c + 1)}>+1</button>
        <button onClick={() => setCount(c => c - 1)}>-1</button>
      </div>

      <div style={{ marginTop: '20px' }}>
        <input
          value={name}
          onChange={e => setName(e.target.value)}
          placeholder="Tapez quelque chose..."
        />
        <p>Nom actuel: "{name}"</p>
        <p>Nom précédent: "{previousName || 'N/A'}"</p>
      </div>
    </div>
  )
}

Ressources Pour Aller Plus Loin