Timo Wernars

Full Stack Software Engineer

Global State in React Without Re-rendering the Entire Component Tree

I like using the React Context API. It's intuitive, easy to implement, and easy to understand. It’s my go-to whenever I need some kind of global state that I don’t want to drill down through child components. However, it's important to realize that whenever you're using a Context with state, all components wrapped in the provider will re-render when that state changes.

Modern browsers usually handle these re-renders without smoothly, especially in smaller apps. But when you're working with large data sets or deeply nested components, it can become a performance bottleneck.

Visualizing mass re-renders

Let’s look at an example where we use a React Context to store a global state. The implementation would look something like this:

const GlobaleStateContext = createContext<{
  active: number
  setActive: (active: number) => void
}>({
  active: 0,
  setActive: () => {},
})

export function App() {
  const [active, setActive] = useState(0)

  return (
    <GlobaleStateContext value={{ active, setActive }}>
      {Array.from({ length: 50 }).map((_, index) => (
        <Node key={index} id={index} />
      ))}
    </GlobaleStateContext>
  )
}

function Node({ id }: { id: number }) {
  const { active, setActive } = useContext(GlobaleStateContext)

  return (
    <div
      onClick={() => setActive(id)}
      className={cn({ active: active === id })}
    >
      {id}
    </div>
  )
}

The code above renders a list of 50 nodes. When you click on a node, the global state gets updated causing the entire component tree to re-render (which is visualized with a red flash animation).

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

Recreating it without mass re-renders

To avoid mass re-rendering, we need to prevent state changes from going through React's internal state mechanism. Instead, we can move global state management outside of React by using a custom Store. This Store holds the global state and lets child components subscribe to updates individually.

type Listener<S> = (s: S) => void

class Store<State> {
  public state: State
  private listeners: Set<Listener<State>>

  constructor(state: State) {
    this.state = state
    this.listeners = new Set()
  }

  public subscribe = (fn: Listener<State>) => {
    this.listeners.add(fn)
    return () => {
      this.listeners.delete(fn)
    }
  }

  public update = (newState: State) => {
    this.state = newState
    this.listeners.forEach((l) => l(newState))
  }
}

We can now store this new Storage instance in a context and provide it to all child components.

const GlobaleStateContext = createContext<{
  store: Store<{ active: number }>
}>({
  store: new Store({ active: 0 }),
})

export function App() {
  const [store] = useState(() => new Store({ active: 0 }))

  return (
    <GlobaleStateContext value={{ store }}>
      {Array.from({ length: 50 }).map((_, index) => (
        <Node key={index} id={index} />
      ))}
    </GlobaleStateContext>
  )
}

We can now use the Storage by reading it from the global Context. Each node will track its own active state locally and update only when relevant. We achieve this by subscribing to the global store and updating the local state only if the current node is the one affected. This way, only the state of the pre-selected, and newly selected nodes will change, and thus be re-rendered. All other nodes will remain unchanged.

function Node({ id }: { id: number }) {
  const { store } = useContext(GlobaleStateContext)
  const [active, setActive] = useState(store.state.active === id)

  useEffect(() => {
    const unsubscribe = store.subscribe((state) => {
      setActive(state.active === id)
    })

    return () => unsubscribe()
  }, [])
  return (
    <div
      className={cn({ active: active === id })}
      onClick={() => store.update({ active: id })}
    >
      {id}
    </div>
  )
}

As a result, only the node that gets clicked updates its local active state and re-renders—nothing else is touched.

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49