React - Custom Hooks: Reusability

Custom hooks allow you to extract component logic and reuse it everywhere. It's one of the most powerful tricks in React! Note that this is useful when you want to extract logic from a place that is used in multiple places!


What is a Custom Hook?

Definition

A custom hook is a JavaScript function that:

  1. Starts with "use" (mandatory convention)
  2. Can use other hooks (useState, useEffect, etc.)
  3. Returns reusable logic
  4. Follows the rules of hooks
// ✅ Basic Custom Hook
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 }
}

// Usage
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>
  )
}

Simple Utility Hooks

useToggle - Toggle a Boolean

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
    }
  ]
}

// Usage
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 ? 'Hide' : 'Show'} content
        </button>
        {isVisible && <p>🎉 Content visible!</p>}
      </div>

      <div>
        <button onClick={simulateLoading} disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Simulate loading'}
        </button>
      </div>

      <div>
        <button onClick={darkMode.toggle}>
          {isDarkMode ? '☀️ Light mode' : '🌙 Dark mode'}
        </button>
      </div>
    </div>
  )
}

useLocalStorage - Automatic Persistence

function useLocalStorage(key, initialValue) {
  // Retrieve initial value from localStorage
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(`localStorage error for "${key}":`, error)
      return initialValue
    }
  })

  // Function to save to localStorage
  const setValue = useCallback((value) => {
    try {
      // Allow value to be a function
      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(`localStorage save error for "${key}":`, error)
    }
  }, [key, storedValue])

  // Function to delete
  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key)
      setStoredValue(initialValue)
    } catch (error) {
      console.error(`localStorage deletion error for "${key}":`, error)
    }
  }, [key, initialValue])

  return [storedValue, setValue, removeValue]
}

// Usage
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>User Preferences (persistent)</h3>
      
      <div>
        <label>
          Name:
          <input
            value={name}
            onChange={e => setName(e.target.value)}
            placeholder="Your name..."
          />
        </label>
        <button onClick={removeName}>Remove name</button>
      </div>

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

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

      <div>
        <label>
          Language:
          <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>✅ All data is saved automatically!</p>
        <p>Name: {name || 'Not defined'}</p>
        <p>Theme: {theme}</p>
        <p>Settings: {JSON.stringify(settings)}</p>
      </div>
    </div>
  )
}

usePrevious - Previous Value

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

// Usage
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>Current count: {count}</p>
        <p>Previous count: {previousCount}</p>
        <p>Difference: {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="Type something..."
        />
        <p>Current name: "{name}"</p>
        <p>Previous name: "{previousName || 'N/A'}"</p>
      </div>
    </div>
  )
}

Resources For Further Learning