Open markerikson opened 3 years ago
Original author: BS-Harou @bsharou
Original date: 2020-05-17T13:14:13Z
This is really nice summary, thank you for putting it together!
In regards to using memo/PureComponents everywhere:
It's possible that trying to apply this to all components by default might result in bugs due to cases where people are mutating data rather than updating it immutably.
In my experience, the decision to use memo/PureComponents everywhere goes together with enforcing immutability of all props passing through React and so if someone is mutating such a value it is a mistake on behalf of the developer/code reviewer/bad types rather than the arch. decision itself.
As a reader I also feel a bit confused on whether you think memoizing everything is a good idea or not where on one hand you argue you in most cased don't have to and you should think about each case but but on the other you say that using it everywhere is probably overall net positive.
I would personally argue that using it every time everywhere is probably the way go. Of course by every time I don't mean when a developer is prototyping or playing with code locally or putting together some examples, but I am talking about full blown production SPAs.
Also in regards to "everywhere" I guess there are some exception, so I guess it is fair to say everywhere but
1) connected components (since Redux does it already)
2) components receiving children (since those aren't usually memoized)
3) components that should rerender every single time
Though again you might be able to argue that using it even in these cases might be better for consistency, so that you don't forget it somewhere important and junior devs don't accidentally copy paste the component and forget about it.
Original author: fabb
Original date: 2020-05-22T20:53:10Z
Thanks a lot for the insights, I learned a few more details!
I have one suggestion and one detail question.
First the suggestion. You mentioned this:
> Currently, there is no way for a component that consumes a context to skip updates caused by new context values, even if it only cares about part of a new value.
This is not entirely true, there are a few implementations of useContextSelector which only cause rerenders when the selected part of the context changed (using `observedBits`), and there is even an RFC open to integrate it into core React: https://github.com/reactjs/...
Second the question. Suppose we pass an onClick function down like in your example, but without a memoized child component, and it‘s assigned to the onClick of a <button> component:
function ParentComponent() {
const onClick = () => {
console.log("Button clicked")
}
return <button onclick="{onClick}"/>
}
Does it make a difference when we use useCallback for onClick, or pass down onClick directly? As far as I have understood it, the render phase would rerender the same components in both cases, but the commit phase is different, as without useCallback, the onclick handler of the button html element in the dom would need to be updated on every render. Is this true? Is this performance-relevant and warrant the use of useCallback?
Thanks,
Fabian
Original author: AndyYou @andyyou
Original date: 2020-05-29T09:26:57Z
About `Memoize Everything?` section. I don't really get the meaning "only if it's going to make a difference in behavior for the child".
Could you give me some examples for that. The next explanation about useEffect as well.
Is that means new reference will make child component get different result?
Original author: Gadi Tzkhori @gadi_tzkhori
Original date: 2020-05-30T09:02:12Z
isn't contextValue as an object, recreated every rerender?, thus requires useMemo wrapping?
Original author: Ganesh Pendyala @ganeshlakshmi
Original date: 2020-05-30T11:03:28Z
Many Thanks for putting this together Mark. It really hardened my understanding of the React rendering behaviour and the pitfalls while using Context.
Original date: 2020-05-30T15:41:19Z
That's exactly the point I'm trying to make throughout the article. Yes, in general, you probably want to memoize your context values, but there's other factors that play into whether or not the rest of the components render. If you don't have anything else blocking renders between the parent that renders the context provider, and the child that consumes the context, the child will always render anyway due to the default recursive rendering behavior.
Original date: 2020-05-30T15:43:01Z
Sort of. Multiple components may be flagged as "dirty" and needing to be re-rendered, all in one event tick. React will then iterate over the entire tree during a single batched render pass. Any component flagged as dirty will definitely re-render, and React will recursively re-render any children of those components
Original author: Dennis Cual @denniscual
Original date: 2020-06-22T07:21:28Z
It means that there's no point to memoize data like function if in the first place, it can never help and could just add some little overhead because of memo process. Like if you use the function object to the "host components" like div because it doesn't render anything than to itself. Or passing not memo function object to a Component but the perf doesn't affect.
Original author: Dennis Cual @denniscual
Original date: 2020-06-22T07:36:56Z
Imo, referencing the unstated solution, in the official React documentation, to this blog is not a good choice like you said the "observedBits" because theres a possibility that it would change in the future. And about your question in button onClick handler, in React reconciliation process the button will be the same but it will only mutate the onClick prop. It means that engine will not destroy the button element rather will reuse the same button element then update onCLick handler which is not the expensive at all and will not lose some dom state like focus, etc.
Original author: Benjamin S. @benjamin_such
Original date: 2020-08-31T08:27:53Z
Hey @markerikson:disqus, really great article! What I really struggled with was/is the memoization of objects. A classic example would be something like:
// We receive `customConfig` from a prop
const { config } = useContext(SomeContext)
const mergedConfig = useMemo(() => ({ ...config, ...customConfig }), [config, customConfig])
In the past I thought this would help, but it doesn't (obviously now) since shallow comparison does not realize it's the same object and will recalculate `mergedConfig`. I feel like this approach is wrong, because I don't see a solution except extracting every key from `customConfig` and put it into the dependency array which sounds absolutely horrible lol. Can you help me in this regard?
May I ask how you gathered all that knowledge? Was your initial motivation just pure interest in React and thus read all the code? I'm really curious how you approach learning all this, maybe I can take something with me :P
Original author: Benjamin S. @benjamin_such Original date: 2020-08-31T08:27:53Z
Hey @markerikson:disqus, really great article! What I really struggled with was/is the memoization of objects. A classic example would be something like:
We receive "customConfig" from a prop
const { config } = useContext(SomeContext)
const mergedConfig = useMemo(() => ({ ...config, ...customConfig }), [config, customConfig])
In the past I thought this would help, but it doesn't (obviously now) since shallow comparison does not realize it's the same object and will recalculate
mergedConfig
. I feel like this approach is wrong, because I don't see a solution except extracting every key fromcustomConfig
and put it into the dependency array which sounds absolutely horrible lol. Can you help me in this regard?May I ask how you gathered all that knowledge? Was your initial motivation just pure interest in React and thus read all the code? I'm really curious how you approach learning all this, maybe I can take something with me :P
I think I the answer on my own, which is: There is no way to memoize objects and make sure to memoize values coming from props
as much as possible. And one could also use react-fast-compare
and do a isEqual
check in useMemo
.
Really useful react bible @markerikson 👍
Good read. Thank you.
Thanks for the guide. It's great!
Thank you so much for that. I have a question tho - you keep stating that react doesn't really care about change to props in regard to rerendering. But in the excellent lifecycle diagram you attached to, it shows that what cause render is both setState and new props.
What cause the difference between the 2? thanks!
Hey Mark, I'm really really appreciate all your awesome works on software engineering.
I got a question when reading the following paragraph:
Similarly, note that rendering <MemoizedChild><OtherComponent /></MemoizedChild>
will also force the child to always render, because props.children is always a new reference.
What I unstanding is that you wanna us to be attentive to writing <MemoizedChild><OtherComponent /></MemoizedChild>
, if the OtherComponent's type is a new reference in the parent render, not only the OtherComponent will get remounted, but also the MemorizedChild
will be rerendered everytime and which is a kind of wasted work.
Does my unstanding is correct?
@YagamiNewLight : not quite.
If we have:
function Parent() {
return <MemoizedChild><OtherComponent /></MemoizedChild>;
}
In that case, OtherComponent
is the same component each time, so that won't cause anything to be unmounted.
However, each time Parent
renders, it passes a new element reference into MemoizedChild
as props.children
(ie, {type: OtherComponent}
). So, MemoizedChild
won't ever get to skip a re-render, because at least one of its props is always changing.
The real point here is that if you use props.children
in your component, there's no point in wrapping it in React.memo()
.
Pardon me please.. I don't get why However, each time Parent renders, it passes a new element reference into MemoizedChild as props.children (ie, {type: OtherComponent}).
If the OtherComponent
is defined somewhere outside, then it is reference stable between the parent renders(because the parent doesn't create it on every render) and why does each time Parent renders, it passes a new element reference into MemoizedChild
?
@YagamiNewLight: remember that JSX is transformed into React.createElement()
calls, and every call to createElement()
returns a brand new JS object. So:
const el1 = React.createElement(OtherComponent);
const el2 = React.createElement(OtherComponent);
console.log(el1, el2); // {type: OtherComponent}, {type: OtherComponent}
console.log(el1 === el2) // FALSE - they are different references
So, every time Parent
runs, it calls React.createElement(MemoizedChild)
and React.createElement(OtherComponent)
, and those generate new element objects.
OK, I get it. Many many thanks for your patient reply!!!
Firstly great article Mark and thanks for this. I have one doubt. You wrote "React-Redux uses context to pass the Redux store instance, not the current state value". How is the redux store instance different from the current state value? store instance is one big object which is a value, no? Please guide
@aqarain
Store instance is not the classic React state
which is so-called reactive to the UI.
You can think of the redux store instance as just a global plain object which can be read from anywhere, but the change of it won't cause any rerenders of the UI.
So here comes the react-redux
, making the React component subscribe to the global redux store object, and auto-rerender when it changes. Context API is just a way of making the redux store can be read from any component
.
So yes, the store instance is one big object which is a value, but not reactive value
, and react-redux is to make it reactive
to the component.
@YagamiNewLight Thank you so much for this clarification
Great Article! I have one question though, how will state updates work in React 18? How will they batch all setState calls? Will they use microtasks to do that?
@tanthongtan: https://github.com/reactwg/react-18/discussions/21 covers the overall intended behavior.
As far as I know, yes, they're using microtasks or something to enable running all queued updates at the very end of the event loop tick.
Thank you for this wonderful article, @markerikson. I have a question about the following statement:
This means that by default, any state update to a parent component that renders a context provider will cause all of its descendants to re-render anyway, regardless of whether they read the context value or not!.
I am not quite seeing this behavior in my app. Here's the top level code from my sample repo:
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ViewStateContextProvider>
<App />
</ViewStateContextProvider>
</React.StrictMode>
);
The ViewStateContextProvider
keeps my viewState
, which consists of a simple boolean indicating if the view is in edit mode or not: type ViewState = { isEditing: boolean };
.
My app has a button that toggles the view state. When I run this app in the React DevTools profiler and click on this button, only the following part of the tree gets re-rendered:
ViewStateContextProvider
Context.provider
...
ViewModeToggle
Notably the App and its other children (HomePage, Header etc.) are not being re-rendered. I have not used React.memo
anywhere in the application. So I am unable to explain why the immediate children of Context.provider
, i.e. the App
, is not being re-rendered (based on the quoted statement above). Any clarification on this would be greatly appreciated.
@nareshbhatia I'm going to guess that your provider component looks like this:
function ViewStateContextProvider(props) {
const [someState, setSomeState] = useState(whatever);
const contextValue = {someState, setSomeState};
return (
<MyContext.Provider value={contextValue}>
// KEY PART HERE
{props.children}
</MyContext.Provider>
)
}
That causes the "same element" optimization that I talked about to kick in, and React will stop recursing as soon as it sess that props.children
was the same as the last time this component rendered.
@markerikson, you hit the nail on the head. I had missed that completely!!! Thank you for clarifying.
@markerikson Love this article! Feels like my itchy and annoying but not reachable point in my back is finally scratched!
Just one little question though. In the "Component Render Optimization Techniques" paragraph's first example code
<div>
<button onClick={() => setCounter1(counter1 + 1)}>Counter 1: {counter1}</button>
<button onClick={() => setCounter1(counter2 + 1)}>Counter 2: {counter2}</button>
{memoizedElement}
</div>
Did you use the same "setCounter1" on both buttons to show that memoized element won't change? I thought that the second button should use "setCounter2" instead of "setCounter1".
Magnificent! Thank you so much for taking the time to write such a detailed review. This has been very helpful to me and I will bookmark this for future reference. Thanks!!
This is a really excellent resource, thanks for writing it down!
If I may offer a few suggestions, there's a few specific topics I would be interested in if you could explore them further:
useSyncExternalStore()
would be very helpful too. This is a topic for which it is also hard to find online resources currently.useSelector()
hooks in a single component, or to create a single selector using createStructuredSelector()
so you only have to use a single useSelector()
hook. I understand that calling the hook multiple times leads to multiple subscriptions, which I would've have assumed could result in multiple renderings prior to React 18. But with automatic batching, does it even matter?@arendjr : good questions!
useSyncExternalStore
may be the original ReactWG discussion ( https://github.com/reactwg/react-18/discussions/86 ) and my PR adding it to React-Redux ( https://github.com/reduxjs/react-redux/pull/1808 ). Also saw https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api .unstable_batchedUpdates()
anyway even if you're not using React 18.@markerikson Thanks a lot, I’ll check those out!
Update: really loved these examples of useSyncExternalStore()
: https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api
thanks a lot. this article covers a lot of details about react
Love this article, thanks a lot!
Veritable 「the guy who writes longest blog article」. Anyway, great article. Thanks.
Hey, @markerikson.
In the <StrictMode>
section you mention
or add logging inside of a
useEffect
hook orcomponentDidMount/Update
lifecycle. That way the logs will only get printed when React has actually completed a render pass and committed it.
According to the docs, both useEffect
and useLayoutEffect
run twice on on mount, so logging would be called twice, isn't that right? Or am I missing something?
An excerpt from the docs:
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
* React simulates effects being destroyed on a mounted component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates effects being re-created on a mounted component.
* Layout effects are created
* Effect setup code runs
@bernardobelchior : hmm, that's a good point. I keep forgetting those get double-run now. The point still stands in general in that renders will run more often, but that does make this harder to log in effects as well.
No problem, just wanted to make sure I understood everything correctly. Great blog post, by the way 🥇
Hey, @markerikson.
I have a big confusion of rendering phase
and commit phase
for Concurrent Rendering
.
As you mentioned on Render and Commit Phases
section, after calculating changes react would apply changes into the DOM.
Also mentioned about concurent rendering
:"This pause the work in the rendering phase to allow the browser to process events....Once the render pass has been completed, React will still run the commit phase synchronously in one step."
And lets see useDeferredValue example of React blog:
function Typeahead() {
const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);
// Memoizing tells React to only re-render when deferredQuery changes,
// not when query changes.
const suggestions = useMemo(() =>
<SearchSuggestions query={deferredQuery} />,
[deferredQuery]
);
return (
<>
<SearchInput query={query} />
<Suspense fallback="Loading results...">
{suggestions}
</Suspense>
</>
);
}
The question being that how it is possible that while input is updating and we can see its changes, the other component (suggestions) has a previous value ? I mean React will apply change into the DOM when it makes sure that all rendering is completed but here we see that commit phase
has happened before rendering suggestions
component by last value!
We can see new changes on the browser when commit phase
happens yes?
@mohammadsiyou : yeah, in that case React is going to run two different render passes + commit phases. It runs one render phase with the "old" data, and commits the UI with those changes, then runs a second render pass with the "new" data and commits that.
Hey! @markerikson
I still don't get why a specific component is getting rerendered even though the value that is being used is primary and identical. I am going to paste here the codesandbox that I created if you don't mind. Thank you in advance this post is awesome!
https://codesandbox.io/s/react-rerender-0x7nb2?file=/src/App.tsx
Render logic must not: Can't mutate existing variables and objects Can't create random values like Math.random() or Date.now() Can't make network requests Can't queue state updates
the double negatives here are a bit confusing
Hi! I want to experiment a bit, and now I cannot understand the reason why the number becomes 5 instead of 1 after the first click. I know "state updates may be asynchronous", but why it's scheduled so that the outer setState
executes before the inner setState
? Can I assume that all the outer setState
will be executed before the inner setState
? Thanks!
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => {
setNumber(5);
return n + 1;
});
}}>Increase the number</button>
</>
)
}
@markerikson Thank you for such an effort!
Can you clarify, please, these parts:
The first pass will batch together setCounter(0) and setCounter(1), because both of them are occurring during the original event handler call stack...
However, the call to setCounter(2) is happening after an await. This means the original synchronous call stack is done, and the second half of the function is running much later in a totally separate event loop call stack...
What do you mean by 'original event handler call stack' ? call stack that exists in particular event loop tick ? If so, 'in a totally separate event loop call stack' probably means call stack in totally separate event loop tick.. Or there are different queues completely ?
Original author: Anujit Nene @anujitnene
Original date: 2020-05-17T10:25:09Z
Totally loved reading this consolidated article, nailed the concepts to clarity! One doubt I have - In the example of context provider (the one before the optimized example using MemoizedChildComponent), if there's a setState call in the ParentComponent, will there be two renders of the subtree scheduled - one due to the setState itself and another one due to the change in the context value provided to the provider due to this setState? Is this understanding correct?