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:
- Starts with "use" (mandatory convention)
- Can use other hooks (useState, useEffect, etc.)
- Returns reusable logic
- 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>
)
}