Optimizing Lifting State Up in React

Lifting state up is a fundamental pattern in React for sharing data between components, but it often leads to performance bottlenecks due to unnecessary re-renders. This article explores actionable techniques to optimize lifted state, including component composition, memoization, state colocation, and utilizing split React Contexts to ensure your applications remain fast and responsive.

The Problem with Lifting State Up

When you lift state up to a common ancestor, any change to that state forces the parent component to re-render. Consequently, every child component nested inside that parent also re-renders by default, even if those children do not consume the lifted state. In large applications, this behavior can cause noticeable UI lag.

1. Optimize with Component Composition

One of the simplest ways to prevent unnecessary re-renders is to restructure your component hierarchy using component composition. Instead of rendering child components directly inside a state-heavy parent, you can pass them as children.

// Before: ChildComponent re-renders every time state changes
function Parent() {
  const [state, setState] = useState(false);
  return (
    <div>
      <input type="checkbox" checked={state} onChange={() => setState(!state)} />
      <ChildComponent />
    </div>
  );
}

// After: ChildComponent does not re-render because it is passed as a prop
function StateHolder({ children }) {
  const [state, setState] = useState(false);
  return (
    <div>
      <input type="checkbox" checked={state} onChange={() => setState(!state)} />
      {children}
    </div>
  );
}

function App() {
  return (
    <StateHolder>
      <ChildComponent />
    </StateHolder>
  );
}

By passing ChildComponent as children, React recognizes that its props haven’t changed relative to the StateHolder render trigger, sparing it from a re-render.

2. Implement React.memo and useCallback

If you cannot restructure your components, you can use React.memo to skip re-rendering a component if its props have not changed.

However, if you pass functions (like state setters) as props to these memoized components, you must wrap those functions in useCallback inside the parent component. Otherwise, a new function reference is created on every parent render, breaking the memoization.

import React, { useState, useCallback } from 'react';

const ExpensiveChild = React.memo(({ onChange }) => {
  return <button onClick={onChange}>Click Me</button>;
});

function Parent() {
  const [state, setState] = useState(0);

  // useCallback ensures the function reference remains stable
  const handleClick = useCallback(() => {
    setState((prev) => prev + 1);
  }, []);

  return (
    <div>
      <p>Count: {state}</p>
      <ExpensiveChild onChange={handleClick} />
    </div>
  );
}

3. Practice State Colocation

Before lifting state up, ask if every component genuinely needs access to that data. State colocation is the practice of moving state as close to where it is needed as possible.

If only a small, deeply nested branch of your component tree needs the state, down-level the state to the nearest common ancestor of only those specific components, rather than lifting it to the global root.

4. Split React Context Providers

If you lift state up using React Context to avoid prop drilling, a single state change in the provider will cause all consuming components to re-render.

To optimize this, split your context into separate providers: one for the state value and one for the state updater function (dispatch).

const StateContext = React.createContext();
const DispatchContext = React.createContext();

function StateProvider({ children }) {
  const [state, setState] = useState(initialState);

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={setState}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Components that only trigger state updates (like buttons) can consume DispatchContext and will no longer re-render when the actual state value changes.