Skip to content
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

Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior #38

Open
markerikson opened this issue Oct 5, 2020 · 46 comments

Comments

@markerikson
Copy link
Owner

No description provided.

@markerikson
Copy link
Owner Author

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?

@markerikson
Copy link
Owner Author

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.

@markerikson
Copy link
Owner Author

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

@markerikson
Copy link
Owner Author

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?

@markerikson
Copy link
Owner Author

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?

@markerikson
Copy link
Owner Author

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.

@markerikson
Copy link
Owner Author

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.

@markerikson
Copy link
Owner Author

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

@markerikson
Copy link
Owner Author

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.

@markerikson
Copy link
Owner Author

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.

@markerikson
Copy link
Owner Author

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

@benjaminsuch
Copy link

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

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.

Copy link

Really useful
react bible
@markerikson 👍

Copy link

Good read. Thank you.

Copy link

Thanks for the guide. It's great!

Copy link

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!

Copy link

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?

Copy link
Owner Author

@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().

Copy link

YagamiNewLight commented Dec 9, 2021

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?

Copy link
Owner Author

@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.

@YagamiNewLight
Copy link

OK, I get it. Many many thanks for your patient reply!!!

Copy link

aqarain commented Dec 19, 2021

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

@YagamiNewLight
Copy link

YagamiNewLight commented Dec 21, 2021

@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.

Copy link

aqarain commented Dec 21, 2021

@YagamiNewLight
Thank you so much for this clarification

Copy link

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?

Copy link
Owner Author

@tanthongtan: reactwg/react-18#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.

Copy link

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.

Copy link
Owner Author

@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.

Copy link

@markerikson, you hit the nail on the head. I had missed that completely!!! Thank you for clarifying.

Copy link

@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".

Copy link

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!!

@arendjr
Copy link

arendjr commented Oct 7, 2022

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:

  • I know concurrent mode rendering can result in "tearing" if one is not careful, especially when using data from external sources. I kinda understand how it works in the abstract, but an example to see how this would occur in practice (with advice on how to prevent it) would be really appreciated!
  • I think it's related to the previous point, but more in-depth exploration on how to use useSyncExternalStore() would be very helpful too. This is a topic for which it is also hard to find online resources currently.
  • A really niche question: When using Redux, is it better to use multiple 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?

@markerikson
Copy link
Owner Author

@arendjr : good questions!

@arendjr
Copy link

arendjr commented Oct 7, 2022

@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

Copy link

thanks a lot. this article covers a lot of details about react

Copy link

mksglu commented Oct 16, 2022

Love this article, thanks a lot!

Copy link

Veritable 「the guy who writes longest blog article」. Anyway, great article. Thanks.

Copy link

Hey, @markerikson.

In the <StrictMode> section you mention

or add logging inside of a useEffect hook or componentDidMount/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

@markerikson
Copy link
Owner Author

@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.

@bernardobelchior
Copy link

No problem, just wanted to make sure I understood everything correctly.
Great blog post, by the way 🥇

Copy link

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?

@markerikson
Copy link
Owner Author

@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.

@sergiovanacloig
Copy link

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

Copy link

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

Copy link

Alecton4 commented Sep 6, 2023

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>
    </>
  )
}

Copy link

naritai commented Sep 22, 2023

@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 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests