-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why calculateChangedBits = () => 0 #29
Comments
So lemme see if I understand what you're actually doing here:
That sound about right? It seems like this would still require React to traverse the entire component tree to find any potential context consumers, although none of them would match the bitmask. That's probably not as expensive as an actual render, but it's still O(n) with the size of the tree. |
Yes.
My guess is not. It seems like the Provider doesn't traverse the entire tree, but each component just "pull"s the context value. No evidence. How could we prove that? What I can tell at this moment is that one benchmark (just one, which is js-framework-benchmark) shows the same or slightly better result compared to the store context (I mean comparison between RRR v4 and RRR v3). |
Or, traversing the entire tree and matching the bitmask is just extremely fast. |
@markerikson I'm not very familiar with React internal code, but this seems it handles changedBits=0 specially. |
Ah, good catch! I thought there might be some special handling there, but I hadn't dug into the code to see if that was actually the case. I'd still expect direct subscriptions to be faster for sure when no components would need to re-render, and I'd generally expect them to still be faster anyway when some do need to re-render because fewer total components would be rendering. Still, good to know this. |
OK, glad to hear that you got what I've learned. Personally, I'm comfortable with this new implementation. (react-tracked has to use this technique anyway, because it doesn't have any external store.) My intuition is that the performance would be comparable. Again, one benchmark showed fairly good numbers. As for benchmark, do you have any plan to update react-redux-benchmarks for hooks?
Just curious, fewer means just minus one, doesn't it? |
Imagine a scenario where a state change would actually cause, say, 10% of the components to re-render. With direct subscriptions, 100% of subscriber callbacks run, but only 10% of the components mark themselves as needing to render. So, React can skip over rendering for large portions of the component tree. With the Not sure how much actual meaningful difference that gives, but it does seem like direct subscriptions would result in somewhat less work overall. As for benchmarks... good question. @MrWolfZ said he forked our benchmarks repo and hooks-ified it during the hooks development cycle, but I haven't looked at that fork or tried to integrate any of the changes back into the main repo. I've been mostly on vacation the last few weeks, and when I get back, my goal is to focus on Redux Starter Kit for a while. So, the benchmarks repo isn't a main priority for me atm. Would happily accept help on that, of course :) |
No, I believe not. Only 10% of the components would have to render with the a) Because otherwise, I wouldn't have gotten the comparable benchmark result. b) Imagine the normal context usage. Remember const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={[theme, setTheme]}>
...
</ThemeContext.Provider>
);
};
const MyComponent = () => {
const [count, setCount] = useState(0);
const [theme] = useContext(ThemeContext);
const backgroundColor = theme === 'dark' ? 'gray' : 'white';
return (
<div style={{ backgroundColor }}>
{count} <button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}; If the Hm, you mentioned "wrapper" components. Could you elaborate what you mean by that? @markerikson |
Oh, wait, I misunderstood something very important about your implementation. I saw you were putting
const { state, subscribe } = useContext(customContext);
const selected = selector(state);
// Provider
listeners.current.forEach(listener => listener(state));
// components
const callback = (nextState) => { That's totally different, then :) Somehow I got the idea that the subscriber callbacks would immediately call |
OK. Yeah, the callback part is not changed in that sense (compared to the store context value approach), and we still have the stale props issue in useSelector. (It would have been a big plus, otherwise.) Now, as you get the idea, my question is how you like this approach. If I may ask, could this be taken seriously before RR7 is released? |
Yeah, it would have been something to consider. Not sure if it would have been the ultimate solution, but would definitely have been worth comparing. |
I read this comment again.
I knew this behavior, but this means that the current approach works in the concurrent mode because it de-opts to sync mode, so it doesn't get the benefit of the concurrent mode. I hope reactjs/rfcs#119 solves this. |
OK, I've updated my tool to check tearing: https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode Now, it measures how long it takes for each action (which is clicking two buttons).
|
awesome !how to use calculateChangedBits = () => 0 |
It make the rerender function of context not work at all. so this library can use it's own way to render the component. |
Background
React-Redux v6 (RR6) introduced what I'd call the state-based context value.
The idea was to let React propagate re-render instead of triggering re-render from outside of React. This makes it easy to ensure components render top-down the component tree.
Unfortunately, a way to bail out with useContext didn't come, as I understand because there's no way to implement that in an efficient way.
So, React-Redux v7 (RR7) went back to store context and subscriptions.
Problem (Hypothetical)
We are not sure what the final concurrent mode will look like, but @markerikson had a comment in the code.
My understanding is if a component reads a state from the store, it might not be consistent for all components in the tree. If React pauses rendering in the middle of the tree, Redux may update the store state. So, a component A and a component B could get different state even in a single render in the tree. I was hoping batchedUpdates solves this, but unless batchedUpdates run all renders synchronously, Redux has a chance to update the store state.
If we could only read a state from the context like RR6, this problem should be solved.
That doesn't mean all issues around concurrent mode are solved. a warning comment by @gaearon .
Solution
We specify
calculateChangedBits = () => 0;
so that React doesn't propagate updates.Only a single Provider subscribes to the Redux store. All components under the Provider subscribe to the Provider. When the store state is updated, Provider re-renders which triggers listeners, subscribed components check updates (in useSelector) and forces render. When a component re-renders, it will read the state from the context, and we expect it's consistent. (No evidence, but that's how RR6 would have worked.)
Note, this still doesn't solve the stale props issue in useSelector.
Regret
If we had this solution half a year ago, we would have been able technically to base RR6 to add hooks support. (Updating peer deps might still require major version up, though.) It's too late and this doesn't provide any constructive suggestion now, but it may give a hint hopefully.
Final note
I read once again the 14110 issue, and found @sebmarkbage 's comment.
It's already noted. I didn't have enough understanding back then. But, this is it.
The text was updated successfully, but these errors were encountered: