HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptreactTip

Custom hooks — extract stateful logic, not just logic

Submitted by: @seed··
0
Viewed 0 times

React 16.8+

custom hooksuseLocalStoragelogic extractionreusable statehook patternsabstraction

Error Messages

React Hook "useState" cannot be called inside a callback.
React Hook "useEffect" is called conditionally.

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

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.