patternjavascriptreactTip
Custom hooks — extract stateful logic, not just logic
Viewed 0 times
React 16.8+
custom hooksuseLocalStoragelogic extractionreusable statehook patternsabstraction
Error Messages
Problem
Developers extract functions into custom hooks just because they use useState, even when the hook doesn't encapsulate a reusable behavior. Or they put non-hook logic inside custom hooks unnecessarily, adding the hook call overhead and the 'must be called at top level' restriction.
Solution
Create custom hooks when you have a reusable stateful behavior pattern:
// GOOD custom hook: encapsulates a complete behavior
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, setValue];
}
// GOOD: reused in multiple places
const [theme, setTheme] = useLocalStorage('theme', 'dark');
const [userId, setUserId] = useLocalStorage('userId', null);
// BAD: wrapping a pure calculation — just export a function
function useDouble(n) { // NOT a hook — no state or effects
return n * 2;
}
// Better: export double as a plain function
// GOOD custom hook: encapsulates a complete behavior
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, setValue];
}
// GOOD: reused in multiple places
const [theme, setTheme] = useLocalStorage('theme', 'dark');
const [userId, setUserId] = useLocalStorage('userId', null);
// BAD: wrapping a pure calculation — just export a function
function useDouble(n) { // NOT a hook — no state or effects
return n * 2;
}
// Better: export double as a plain function
Why
Custom hooks are composable units of stateful behavior. They must start with 'use' so React's linter can enforce the Rules of Hooks. The value is in sharing the state + effect combination, not just abstracting any function. Plain functions are simpler when no hooks are involved.
Gotchas
- Custom hooks must start with 'use' — React's linter uses this to enforce Rules of Hooks
- Each call to a custom hook creates independent state — hooks are not singletons
- Return both values and setters as a tuple [value, setter] or object {value, update, reset}
- Don't extract into a custom hook prematurely — three or more uses is a good signal to extract
Code Snippets
useFetch custom hook
// Complete custom hook pattern
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(r => r.json()).then(setData).catch(e => {
if (e.name !== 'AbortError') setError(e);
}).finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}Context
When deciding whether to extract logic into a custom hook or a plain function
Revisions (0)
No revisions yet.