RFC: selective context propagation
See original GitHub issueSelective context propagation
I see the comparison of Redux to Context quite often lately and wanted to act on this.
Differences
Redux is performant by nature, it uses the concept of selectors to memoize values and rerender selectively, context on the other hand will propagate through to every Consumer
subscribed to the whole context.
The clear difference between these two we can notice is that when x amount of components are subscribed to individual pieces of state, for redux this will rerender the amount of times the state has changed while with the current form of context this will be x.
Prerequisites
One of the big prerequisites we’re lacking out for this concept to succeed is strict-equality on vnodes, this means that if the children
did not change from render 1 --> render 2 we won’t have to evaluate these children. This is a very common pattern when it comes to contextProviders.
Let’s consider the following scenario:
const App = () => (
<MyProvider>
<MyRoutes />
</MyProvider>
);
When <App />
updates this whole tree will rerender since the function will execute and reevaluate all these children, let’s for clarity look at a hypothetical <MyProvider />
implementation.
const { Provider } = createContext();
const MyProvider = ({ children }) => {
const [state, setState] = useState();
return (
<Provider value={{ state, setState }}>
{children}
</Provider>
)
}
As we can see there’s a prop injected named children
, if this doesn’t change the vnode is equal and shouldn’t be diffed. This means that if a Consumer
calls setState
the MyProvider
component will alter its internal state but the children
, injected by <App />
(which hasn’t reexecuted), will still be the same so we can safely decide to not diff this part.
This means that instead of walking the tree downwards we’ll only trigger the contextConsumers
.
Implementation
To come back to our selective context propagation, we have two options to support this effectively.
Option 1
We can supply a rerender function similar to shouldComponentUpdate
, useContext
/others could allow a second argument (or a new hook) that allows the user to selectively compare the old and new context and decide wether this hook needs an emit. This would always return the full context (important note when reading the next proposition).
const MyName = () => {
const { name } = useContext(userContext, (prev, curr) => prev.name !== curr.name);
return <p>Welcome {name}</p>;
}
Option 2
Selector-style, for this case useContext
/others can accept an argument that selects a certain value out of context, if this value hasn’t changed compared to the previous instance it won’t cause a rerender. The return value of useContext
/others will be the selected value as opposed to the full context.
const MyName = () => {
const name = useContext(userContext, ctx => ctx.name);
return <p>Welcome {name}</p>;
}
Concluding
Option 1 will probably be the smallest, size-wise and introduce less context
retrievals in general. This means that we have less calls to useContext
/less nesting of context.Consumer
since if we’d need more properties we’d need to introduce more selectors (or complex ones). Less useContext
calls means less comparisons which can only benefit performance + for option 1 we have the current infrastructure ready to go.
This RFC aims at bringing out of the box perf improvements to Preact.createContext
not a replacement of Redux
. There are many things that are in Redux that would have to be reimplemented in user land (in the form of libraries potentially) to replace for instance, logging, …
Issue Analytics
- State:
- Created 4 years ago
- Comments:7 (4 by maintainers)
Top GitHub Comments
@robertknight i think that also happens with Redux, At least when i worked with Redux last time, i remember having some issues with the return of mapStateToProps. What redux does is that it always assume you return a object and do a shallow equals to avoid that rerender, but still you can mess with object equality at a different depth and get too much rendering.
Thanks a lot for all of this information @gnoff I’ll be certain to look into all of this in the near future, we’ll clear this up asap.
Also thanks a lot for pointing out a bug, we never thought about this case https://github.com/preactjs/preact/pull/2501