loading...

Context, state and rerenders

filipesmedeiros profile image Filipe Medeiros ・3 min read

Hey guys! I have a topic I'd like to ramble about and also know your opinions on.

We all know Context. An we know it can (but sometimes shouldn't) be used to provide some sort of global state. But there's normally a problem: controlling rerenders. Let's dig a bit deeper.

How to use Context for global state

Again: everyone probably knows this, but Context just provides a value to every component below the Provider. So we could just do this:

...
<Context.Provider value={0}><Chidlren /></Context.Provider>
...

Now, of course we want to make this value dynamic. Having a fully static value as the Context's value makes it more of a config value that app state.
So, to make it dynamic, we just have to assign it to a variable, right?

const Wrapper: FC = () => {
    let counter = 0
    const inc = () => counter++
    return <Context.Provider value={counter}><Chidlren /></Context.Provider>
}

But you may have noticed that counter is not state. So changing counter (by using inc) won't cause a render on Wrapper and, therefore, on Children.

"Easy to solve, just use state!" Fair enough, let's try that:

const Wrapper: FC = () => {
    const [counter, setCounter] = useState(0)
    const inc = useCallback(() => setCounter(c => c + 1), [setCounter]) // Using useCallback is not necessary
    return <Context.Provider value={counter}><Chidlren /></Context.Provider>

Now, if we call inc, the Wrapper's state will change and it will render, passing a new value to the Context.Provider and the Children to also render with this new value.

The new problem

But wait: aren't Providers supposed to be relatively high up in the tree? And isn't updating their state gonna cause everything below them to render? Well, yes. And we don't want that.

Say you have this structure:

<Wrapper />
// which renders
<Context.Provider /> // provides counter
// which renders
<ChildDeep1 />
// which renders
<ChildDeep2 />
// which renders
<ChildDeep3 /> // only this one needs counter

Wow bro, that's deep. I know right? Anyway, if we only need counteron ChildDeep3, this is cause (potentially many) unnecessary rerenders along the tree.

The solution

The solution to this problem is two-fold:
1) maybe it's better to just optimize the renders and let React render the whole thing. If the tree is not too big and making these optimizations is easy, try it. Else,
2) useMemo() to the rescue! Honestly I took way to long to figure this out, but wrapping the first children in a useMemo() prevents it from rendering, but doesn't prevent deeply nested children to update if they consume the Context's value! This is awsome. Now you can have this:

<Wrapper />
// which renders
<Context.Provider /> // provides counter
// which renders
const child = useMemo(() => <ChildDeep1 />, [])
{child}
// ChildDeep1 renders
<ChildDeep2 />
// which renders
<ChildDeep3 /> // only this one needs counter

Small caveat

If you want to pass props directly to the first child of the Provider, you just need to pass them normally (inside the useMemo()) and add them to its dependencies, like so:

const child = useMemo(() => <ChildDeep1 prop={prop} />, [someValue])

Now if prop changes, ChildDeep1 rerenders (and everything below it) as normal.

You can check out a working demo here: https://codesandbox.io/s/intelligent-nobel-jcxeq?file=/src/App.tsx

Conclusion

This pattern should be used in other situations, even if they don't include Context, because it allows to very precisely control how components rerender. In short: hooks are great.

Posted on by:

filipesmedeiros profile

Filipe Medeiros

@filipesmedeiros

Front-end engineer. I'm a designer in the next parallel universe. Board games, frisbee, my dog and freedom.

Discussion

markdown guide