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 Provider
s 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 counter
on 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.
Top comments (0)