Advanced Hooks

While useState and useEffect cover 80% of use cases, React provides powerful advanced hooks for managing complex state, performance, and DOM interactions. Mastering these tools distinguishes a junior React developer from a senior one.

1. useReducer: Managing Complex State

When state logic becomes complex (e.g., multiple sub-values, next state depends on previous state), useState can become unwieldy. Enter useReducer.

First Principles: The State Machine

Think of useReducer as a Finite State Machine. Instead of just setting a variable, you send a signal (Action) to a machine (Reducer), and the machine decides what the next state should be based on its current state and the signal.

This decouples what happened (Action) from how state updates (Reducer).

The Pattern

  1. State: The data source of truth.
  2. Action: An object describing an event ({ type: 'INCREMENT' }).
  3. Reducer: A pure function: (state, action) ⇒ newState.
const [state, dispatch] = useReducer(reducer, initialState);

Interactive: The Reducer Playground

Try dispatching different actions to see how the reducer updates the state. Observe how the logic is centralized.

Dispatch Actions

Current State

{
  count: 0,
  step: 1
}
  
Last Action Dispatched:
None

2. useContext: Dependency Injection

useContext solves the “prop drilling” problem.

// 1. Create Context
const ThemeContext = React.createContext('light');

// 2. Provide Context
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 3. Consume Context
function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>...</div>;
}
⚠️ Performance Pitfall: When a Context Provider's value changes, ALL components consuming that context will re-render, even if they only use a small part of the data. Keep context values small or split them up.

3. useEffect vs useLayoutEffect

These two hooks are identical in signature but differ in timing.

  • useEffect: Runs asynchronously after the paint. (99% of use cases).
  • useLayoutEffect: Runs synchronously after DOM mutations but before the paint.

When to use useLayoutEffect?

Use it ONLY when you need to measure the DOM (e.g., width, scroll position) and then update state before the user sees the screen. If you use useEffect for this, the user might see a “flash” of content jumping.

Decision Flowchart

graph TD A[Do you need to mutate the DOM based on measurement?] -->|No| B(useEffect) A -->|Yes| C{Is it visual?} C -->|Yes| D[useLayoutEffect] C -->|No| B D --> E[Blocks Paint] B --> F[Does Not Block Paint] style A fill:var(--accent-main),stroke:#fff,color:#fff style B fill:var(--green-500),stroke:#fff,color:#fff style D fill:var(--purple-500),stroke:#fff,color:#000

4. useImperativeHandle

Occasionally, a parent component needs to invoke a function directly on a child component (e.g., inputRef.current.focus()). useImperativeHandle allows you to customize what the parent sees.

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    shake: () => {
      // Custom animation logic
    }
  }));

  return <input ref={inputRef} />;
});

Key Takeaways

  • useReducer: Predictable state transitions for complex logic.
  • useContext: Global state, but beware of re-renders.
  • useLayoutEffect: Fixes visual flickers when measuring DOM.
  • useImperativeHandle: Exposes custom methods to parents.