The talks explore various misleading patterns and concepts in React, that seem to be “common knowledge”, but which a lot (if not most) developers either get wrong or are just not aware of those. Some examples, covered in the talk (and more):
* that “react component re-renders when its props change” (this is not true)
* that wrapping a component in React.memo will prevent its re-render (not always true)
* that use of Context causes re-renders and is bad for performance (not always true, sometimes Context can actually reduce the number of re-renders in the app)
* that creating an element like this `const A = <Child />` is when the Child's render lifecycle is triggered (not true)
React Myths And Legends
This talk has been presented at React Summit 2023, check out the latest edition of this React Conference.
FAQ
Nadia's main focus in her investigations for front-end developers is React Performance.
Mounting is the first time a component appears on the screen. This is when React initializes and wires everything together, firing all callbacks and use effects for the first time.
Unnecessary re-renders in React can be caused by re-rendering the entire app or large parts of it when only a small component or piece of data has changed. This can lead to performance issues and slow interactions.
The 'Big Re-renders Myth' in React is the belief that a component re-renders when its props change. This is not true; re-renders are primarily caused by state changes.
Context can help manage re-renders in React by allowing data to be passed directly to the components that need it, bypassing the rest of the component tree. This reduces the number of components that need to re-render, improving performance.
Common misconceptions about the key attribute in React include the belief that it prevents re-renders. The key attribute is used to help React identify which items have changed, been added, or removed, but it does not prevent re-renders.
Array indices should not be used as keys in React because they can lead to performance issues and bugs. When items are added or removed, using indices can cause unnecessary re-renders of all items in the list. It's better to use unique IDs.
When state changes in React, the parent component re-renders, and React propagates this update to its children recursively. This means all direct children and their children will re-render unless explicitly prevented.
Splitting context providers can help with re-renders in React by separating the state data and the functions into different contexts. This way, only the components that depend on the specific context will re-render when it changes, improving performance.
Memoizing a component in React is important because it prevents unnecessary re-renders. Wrapping a component in React.memo ensures that React checks its props and only re-renders if the props have changed, improving performance.
1. Introduction to Myths and Legends in React#
Welcome to the talk about myths and legends in React. My name is Nadia, and I've been a developer for a very long time. Today, I want to share a few myths and misconceptions that can negatively impact re-renders in all apps. But before that, let's quickly remember what is mounting, re-renders, and unnecessary re-renders in React.
Hi, everyone. Welcome to the talk about myths and legends in React. Let me start with a little bit of an introduction. My name is Nadia. I've been a developer for a very long time. I worked for Atlassian for about five years on a product that some of you might know and love called Jira. Until very recently, I lived in Australia just surrounded by parrots and kangaroos. But a few months ago, I got tired from all this nice weather and perfect beaches and I moved to Austria for the only reason that I am lazy and Austria is easier to spell.
Also, I am a bit of a nerd. One of my nerd hobbies is to investigate how things work in detail and then write deep dive articles on those topics. I usually write for front-end developers, more specifically React developers. One of the main focus for those investigations, the thing that interests me the most recently is React Performance. And the topic of performance in React is crazy interesting. React is a fantastic tool that allows us to write complicated applications really easily and quickly. But as a result, it's also very easy to write code that will result in our applications being very slow and lagging. And most of the time, it's because of re-renders. We either re-render too fast or too many or too heavy components. So the key to good performance in React is knowing and being able to control when components are mounting and re-render. And then prevent those that are unnecessary.
So today, I want to share a few myths and misconceptions that are quite common among developers and that can negatively impact re-renders situation in all apps. But before doing that, let's quickly remember what is mounting, re-renders and what are unnecessary re-renders. It all starts with mounting. This is the first time the component appears on the screen. This is when React initializes and wires everything together, thus initial render fires all the callbacks and use effects for the first time. After that, if your app is interactive, it will be time for re-renders. Re-render is when React updates an already existing component with some new data. Those are usually happen as a result of user interacting with your interface or some external data coming through. Re-renders are a crucial part of React lifecycle. Without those, there will be no updates to the interface, and as a result, no interactivity. So it's not something that we would want to get rid of.
2. Unnecessary Re-renders and the Big Re-renders Myth#
Unnecessary re-renders occur when the entire app re-renders on every keystroke, negatively impacting performance. The Big Re-renders Myth states that a component re-renders when its props change, but this is not true. Re-renders are triggered by state changes, which propagate updates to other components recursively.
Unnecessary re-renders, however, is a completely different story. Imagine a React app, just a tree of components like any other app, and somewhere at the bottom of this tree, we have an input-filled component where a user can type something. When this happens, I want to update the state of this input and everything related to the user data, like showing some helpful hints while the user is typing. This is re-render. The last thing that I want, though, is for the entire app to re-render itself on every keystroke. Imagine how slow the typing in this field will be if something like this happens. This is definitely not something anyone would call a performance app. This is an example of unnecessary re-renders, and those are exactly the type of re-renders we would want to get rid of.
And when it comes to preventing re-renders, there is this big myth that somehow everyone believes. I call it the Big Re-renders Myth. It goes like this. A component re-renders when its props change. It's amazing, really. Everyone believes it. No one doubts it. And it's just completely not true. To understand that, let's dig a little bit deeper into why re-renders happen in the first place. It all starts with state change. Any React developer will probably recognize the code. We have a parent component. It renders a child component and it has a useState hook. When setState is triggered, the entire parent component will re-render itself. State change is the initial source of all re-renders. It's the king of re-renders so to speak. That's why it's in the crown. After the state change, it's time for React to propagate this update to other components. React does this recursively. It grabs direct children of a component with state, re-renders those, then re-renders children, and so on until it reaches the end of component's tree or is stopped explicitly. This is the next reason for a component to be re-rendered when its parent component re-renders. If we look at the code, once the state change in parent component happens, all children that this component has will re-render as a result.
3. Re-renders and the Myth of Memoization#
State updates trigger re-renders in the parent component, which in turn cause re-renders in all its children and their children. React doesn't compare props during the normal lifecycle, so memoizing non-primitive props is unnecessary. The memoization of the component itself is crucial for props memoization to work effectively. The myth surrounding context and re-renders is controversial, as context can be a helpful tool in reducing re-renders when passing data through a large component tree.
It would look something like this. State update is triggered. Parent component re-renders. Children re-render, children of the children re-renders, and so on and so forth.
One interesting thing here is, as you can see, I haven't mentioned anything about props here, even once. What will happen if the child component has some props? Nothing really. During normal react lifecycle, react actually doesn't compare props at all. If a child is there, it will be re-rendered.
This leads me to another myth that is derived from the first one, the myth that we should memoize all non-primitive props on a component to prevent it from re-renders. Again, this is a myth, not true. As a result of this belief, it's quite common to see that code that looks like this. A callback is wrapped in useCallbackHook, which is probably okay if it's just one, but then there will be another and another until the beautiful component logic is just buried under this incomprehensible and unreadable mess of useMemos and useCallbacks. For absolutely no reason at all. All of those are just useless and prevent nothing. All of this because we forgot one important step here. Memoizing component itself. In order for props memoization to work as we expect, the component also needs to be memoized. It needs to be wrapped in a React.memo. Only then will React stop before re-rendering and check its props. So this is finally the case where onChange prop needs to be wrapped in useCallback. If none of the props change and only then will React stop and won't re-render further.
Next myth, probably the most controversial one. Context. Context unfortunately has a bad reputation when it comes to re-renders. Some of it of course is deserved. But also because of this reputation, sometimes people not realize that context can be very helpful tool in the fight against re-renders. Let's take a look why? Imagine again a big components tree with one component somewhere at the top wanting to pass some data to the component at the bottom, the pink one. And we just pass this data through props as normal. What will happen from a re-renders perspective? Nothing good. The state changes at the very top, all children are re-renders, their children of children also re-renders until this data reaches the component that actually needs it.
4. Context and Re-renders#
Context allows us to bypass the component tree and avoid unnecessary re-renders. We can extract data and use it directly in components, reducing re-renders. By using context, we can simplify complex component structures and reduce code. However, context can cause re-renders in all child components, even if they don't use the data. Splitting context providers into separate values can help mitigate this issue.
Context, however, allows us to cheat a little bit and bypass that tree of components. If we just extract this data that we want to pass around to context and use it in the pink component directly, then only the component with the data itself and the component that actually uses it will re-render. The rest of the app will just sit there quietly and do absolutely nothing.
Okay, kittens are fun, but let's take a bit closer look at normal life and normal components. Let's say I want to implement a page that looks like this. Two-page layout navigation on the left, main content area on the right. In navigation I want to have a button that expands and collapses navigation. And I want to render different number of blocks at the bottom of the content area depending on whether navigation is expanded and collapsed. If I start implementing it with normal props, it would look something like this. We would have a page that holds navigation state, passes that state to the button that expands it through navigation component and listens to the callback on it. And then we would have to pass this data to the page component through all the layers down, down, down to the level where those blocks that I need to render. And again, from re-render's perspective it's not great. Every navigation expanding or collapsing will result in re-renders of absolutely everything.
What I can do instead here is to extract this expand collapse logic into context, only logic, nothing more. This would look something like this, same state that controls navigation, we would have a value to which we would pass the state itself and the toggle function, and then we would pass this value to context provider. Suddenly our page, instead of tons of code and props everywhere, it just turns into something as clean as this. Context at the top, navigation, and content passed to it as children. And then somewhere in navigation, we would have the button that just uses toggle function directly from context. And somewhere down the content area, we would have the block with items that use the state of navigation directly from context.
But although this example looks cool in theory, in real life it's of course a little bit more complicated, as always. In real life, we still have a problem of context having a bad reputation for re-renders. Mostly this reputation comes from two facts. The first is that context-related re-renders will be triggered in every child that happens to use this context hook, regardless of whether it uses the actual data or not. So in our case, the component on the left that uses only toggle function, in theory, shouldn't care about state at all, but in practice, both of those components will re-render when context changes. And the second problem here is that those re-renders are unstoppable. There is no sane way to prevent them. Use memo or use callback hooks won't help. There is, however, another trick that can help here if this behavior actually causes performance problems. We can just split those context providers into two. Instead of just one single value that holds both toggle and the state, we can have two separate values.
5. Separating State and API in Context#
When separating the expanded state and the API volume, the component that uses toggle won't re-render. Using separate contexts for state data and API functions ensures that components using the API won't re-render. Context may not be necessary in all apps, as state management tools like Redux can be used instead. Understanding context makes state management easier.
And now, when we separate them, when expanded state has changed, the API volume, the object that holds the toggle function, won't change. So the component that uses toggle only won't re-render. In code, it would look something like this.
Instead of this big, monolithic provider, we would have two, under the same roof of the collapsed provider component. We would still have the same state and the same children, but we would have a separate context. We would have a separate context for the state data only, and separate context for the API with a bunch of functions only.
Now, as you can see, the context that holds API doesn't depend on state, so its value won't change with the state update, and as a result, components that use this API won't re-render.
And now the big question here. Should we actually use context in our apps? And the answer is maybe not. In real life, more likely than not, you would use some state management tool like Redux instead of context. But the mental model and re-render's behavior will be exactly the same with those tools, they are just more convenient to use. So instead of context, if you understand context, any state management after this will be a breeze.
6. The Key Attribute in React#
The key attribute in React is often misunderstood. It is used to identify and differentiate children in an array, allowing React to optimize rendering. Contrary to popular belief, the key does not prevent re-renders. Instead, it helps React determine if a child component is the same as before or a new one. By assigning unique keys, React can efficiently update and re-render components when necessary.
And to the final re-render's myth for today, the key attribute. This one is my favorite, it's probably the most used and the least understood feature in React. We use it on a daily basis when we are rendering arrays of items, and YesLint then yells at us that the key is mandatory and we need to put something there. But what all of us actually know about this key, what exactly should go there, what it's for. If you ask your colleagues right now those questions, what will be the answer? It will be probably something like this. They would say the key should be unique and array index shouldn't be used as keys. But why? How unique it should be? Can I just do something like this instead? And if no, then why not?
But if you press with those questions a little bit further, sometimes people will say that we need key to prevent re-renders. And this one is the fun one. Although, yes, key is indeed should be unique. But it has nothing to do with preventing re-renders, usually. This is just a myth. As we know already, if a component re-renders, all of its children will re-render. Whether props change or not, in this case, and in this case, whether the key is there or not, it doesn't actually matter. If component is not memorized, it will re-render. Key won't prevent that. Why do we need it then?
Imagine yourself in a room with a bunch of identical kittens wearing different hats. You don't know their names, so the only way to identify those and describe which cat wears which hat is by their number. The first cat wears a crown, the second one wears a baseball hat, the third one wears a magician hat. This is exactly what React has to deal with when we iterating over an array and rendering children. If we don't name those explicitly, React has no way to know when it renders this component, whether a child is the same child or a completely new one other than using their positions in the array, which is what it uses by default. Unless we name those children explicitly. This is what key is for. When we assign those attributes, and the component that has those items re-renders, React will detect that the children are exactly the same as before. So all it would need to do is re-render them as usual. If, however, we change the key on one of the children, React will think that the old component with the key equal zero is not there anymore. So it will remove it and unmount it. And then it will detect that a new component with the key number six appears. It will think that it's a completely new component. So it will mount it from scratch. As a result, the rest of the components will re-render, but the component with the key changes will remount itself.
7. The Role of Keys in Preventing Re-renders#
Even if nothing else other than key changes, generating a new key on every re-render will result in remounting and various bugs. The key does not prevent re-renders; we need to use methods like react.memo. Using arrays.index as a key is bad practice for dynamic lists, as every item will be detected as changed and re-rendered. Instead, using unique IDs from the data itself ensures that only new items are mounted. For more information, check out the articles and blog on this topic.
Even if nothing else other than key changes. And that actually should answer the question on how unique our key should be. If we do something like this, like generate a new key on every re-render, then on every re-render, every single one of them will remount itself. This will result in various bugs with focus and state. Not to mention that it's much, much slower than normal re-render, so you will have performance issues as well.
As for preventing re-renders, the key has nothing to do with it here. We need to use one of the methods like react.memo in order to prevent children from re-renders. If we use it, now the parent component re-renders and children won't re-render. But it's not because of the key, because of react.memo.
That brings the question, what's the point of key then if it doesn't prevent re-renders? And why everyone says that using arrays.index as key is bad practice? It's because of dynamic lists. Imagine that we add a new item at the beginning of the array. Maybe by clicking a button that adds a new to-do item at the top. What will happen here if we use arrays index as a key? New item is added at the beginning of the array, so it will be the first one now. Every item in this array as a result will be detected as changed. And all of them will re-render, even if they are wrapped in react.memo. If, however, we use unique IDs that we get from the data itself, then when we add the new item at the beginning, only the very first item will be detected as new and mounted. The rest will not change. And if they are wrapped in react.memo, they will not re-render.
There is much more to know and understand about keys and the context and re-renders in general. So here are some helpful articles that I wrote on the topics. And check out the blog itself. It has many, many more react patterns and investigations like this. And if you have any questions or feedback, feel free to reach out on Twitter or on LinkedIn. I'm always happy to answer any questions.
Comments