C.W.K.
Lesson 03 of 06 · published

Custom Hooks + forwardRef Patterns

~14 min · react, hooks, patterns

Level 0Curious
0 XP0/52 lessons0/16 achievements
0/100 XP to next level100 XP to go0% complete

Custom hooks aren't 'advanced' — they're how you organize

The moment you find yourself using two related hooks together in more than one place, extract them. That's the whole rule. The convention is to prefix the function with use so React's lint rules know to treat it as a hook (so the rules of hooks apply: don't call it conditionally, don't call it inside loops).

My chat sidebar has a useConversations() hook. My input has a useCommandPalette() hook. My theme system has a useTheme() hook. None of them are 'advanced' — they're just the natural extraction once a pattern repeats.

forwardRef — when a parent needs the child's DOM node

Refs don't pass through props normally. If you want a parent to control the focus of a child input, you need forwardRef. React 19 simplifies this: function components can accept ref as a regular prop now, no forwardRef wrapper needed in most cases.

useImperativeHandle — expose a custom ref API

Sometimes you don't want the parent to have the raw DOM node — you want to expose specific methods (focus(), scrollToBottom()) without the DOM contract leaking. useImperativeHandle lets the child decide what the ref looks like.

When to use it: Almost never, until you've shipped enough React to feel the pain it solves. If you're reaching for it as your first tool, you're probably building too much imperative state into the view layer.

Code

useTheme — a custom hook from my actual ThemeProvider·tsx
type Theme = 'dark' | 'light' | 'system';

function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    return (localStorage.getItem('theme') as Theme) ?? 'system';
  });

  useEffect(() => {
    const resolved = theme === 'system'
      ? matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
      : theme;
    document.documentElement.dataset.theme = resolved;
    localStorage.setItem('theme', theme);
  }, [theme]);

  return { theme, setTheme };
}
InputArea with forwardRef — parent can focus the textarea·tsx
interface InputAreaHandle { focus: () => void }

const InputArea = forwardRef<InputAreaHandle, Props>((props, ref) => {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  useImperativeHandle(ref, () => ({
    focus: () => textareaRef.current?.focus(),
  }), []);
  return <textarea ref={textareaRef} {...props} />;
});

Progress

Progress is local-only — sign in to sync across devices.