6 min read
0%

Custom Hooks Design Patterns

Back to Blog
Custom Hooks Design Patterns

Custom Hooks Design Patterns

Custom hooks are functions starting with use that compose React’s built-in hooks into reusable, testable units. They’re the primary code-sharing mechanism in functional React.

Basic Extraction

Extract stateful logic from a component into a hook:

// Before: logic tangled in component
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser).catch(setError).finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <Spinner />;
  if (error) return <Error error={error} />;
  return <Profile user={user} />;
}

// After: logic extracted to a hook
function useUser(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetchUser(userId).then(setUser).catch(setError).finally(() => setLoading(false));
  }, [userId]);

  return { user, loading, error };
}

Return Object vs Tuple

Return an object when consumers name the values (common):

const { data, loading, error } = useFetch('/api/users');

Return a tuple when consumers need to rename (like useState):

const [isOpen, setIsOpen] = useToggle(false);
const [isVisible, setIsVisible] = useToggle(true);

Callback Stabilization

Hooks often need stable callback references:

function useDebounce(fn, delay) {
  const fnRef = useRef(fn);
  useEffect(() => { fnRef.current = fn; });

  return useCallback(
    debounce((...args) => fnRef.current(...args), delay),
    [delay] // only recreate if delay changes
  );
}

Subscription Pattern

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    }
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

Imperative Handle Pattern

function useModal() {
  const [isOpen, setIsOpen] = useState(false);
  const open = useCallback(() => setIsOpen(true), []);
  const close = useCallback(() => setIsOpen(false), []);
  const toggle = useCallback(() => setIsOpen(v => !v), []);
  return { isOpen, open, close, toggle };
}

Composing Hooks

Custom hooks can call other custom hooks:

function useUserProfile(userId) {
  const { data: user, loading } = useFetch(`/api/users/${userId}`);
  const { data: posts } = useFetch(user ? `/api/users/${userId}/posts` : null);
  const { width } = useWindowSize();

  return { user, posts, loading, isMobile: width < 768 };
}

Testing Custom Hooks

Use @testing-library/react’s renderHook:

import { renderHook, act } from '@testing-library/react';

test('useCounter increments', () => {
  const { result } = renderHook(() => useCounter(0));
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});

Canvas is not supported in your browser