React has by far been the popular choice for writing User Interfaces, Performance is a important thing to consider in React as things can go out of the way, making your web app feel slow and user’s complaining things are taking time. When we talk about Performance in React, re-renders and their affect is very crucial to understand, How actually re-renders happen, how it flows through your app, what exactly happens during a re-render and why do they exist or we need them in the first.
All of these Question will help us to explore these topics, and to make it interactive and easy to grasp, lets become a detective , we would start out with a very common performance issue in our app, let see what’s going on and how we can try to fix the same, while trying we would be learning
- What exactly a re-render is, why they exist?
- Where does re-render initiate from.
- How React tries to distributes re-renders through an app.
- How moving state down can help us solve performance issues in our app
The Problem
Imagine you are a developer working on a react web app, this is a large production app with a lots of thing going on, you can assume it as a legacy codebase where it has been worked by other developer from the past and it has a huge customer base. You get your first ticket Assigned, you are exited and you got a simple task to warm you up, you need to create a Place order Page that opens a modal at the top of the app.
The code in abstract Looks like below and you get the idea where should the dialog be triggered
const App = () => {
return(
<div className="layout">
<VerySlowComponent/>
<SomeStuff />
<OtherStuffAlsoComplicated/>
</div>
);
};
Seems quite any easy task , you must have done this a gazillion times, kidding probably more times than you can think
const App = () => {
// you would be addding some state
const [isOpen,setIsOpen] = useState(false)
return(
<div className="layout">
// you need to add a button
<Button onClick={() => setIsOpen(true)}>Place Order</Button>
<VerySlowComponent/>
<SomeStuff />
<OtherStuffAlsoComplicated/>
</div>
);
};
Sprinkling some state that hold the dialog is open or closed. add the button which eventually triggers the state update onClick even. and the dialog renders only if the state variable value os true.
You did so and try to test it works, unfortunately you notice there is a slight delay to open that modal.
Many might feel that we need to bring the performance hooks like memo and callback to fix this, this techniques would feel meaningless to include here, there are more simpler things that we can use to solve this issue.
State Updates and Re-renders
Let's start from the beginning: the life of our component and the most important stages of it that we need to care about when we talk about performance. Those are: mounting, unmounting, and re-rendering.
When a component first appears on the screen, we call it mounting. This is when React creates this component's instance for the first time, initializes its state, runs its hooks, and appends elements to the DOM. The end result - we see whatever we render in this component on the screen.”
Unmounting is when React is directed that we no longer need a component, so it does a cleanup and unmounts the component essentially destroying the same, its associated state, hooks and eventually gets thrown out of the DOM elements as well.
Re-rendering means when React updates an existing component with some extra information. if we differentiate it between mounting, re-renders are less workaround since React will use the already existing component alive , doing all the necessary calculations for what changed, runs the hooks and eventually updating the existing DOM elements.
Lets see where Re-renders stem from, the origin point for re-renders is state. In React, when we use a Hook like useState,useReducer or any other state Management Tool like MobX, Redux etc. we introduce some piece of Data that would live with it and would be preserved throughout its lifecycle, if something happens that needs a response like user clicking a button, typing into an input or some external data is coming , the particular state would be updated with the new data.
Re-rendering is an important concept to grasp around react. This is what Allows React to update the component with the new Data and trigger hooks that depend on the same. Without Re-rendering, There will be no updates in React which would result in no interactivity, making the app completely static. so remember state update is the initial source for all renders in React bases app. if we take the example of the code
const App = () => {
const [isOpen,setIsOpen] = useState(false)
return(
<Button onClick={() => setIsOpen(true)}>
Place Order
<Button/>
)
}
Here when We click on the button it triggers a re-render the setIsOpen state setter function is called, updating the state with new value from false to true. thus our App component re-renders as it holds the state.
As the state is updated, our App component renders, new data is delivered to component that depend on it. React does it by its own, it takes all the components that the initial component renders inside, re-renders those, then re-renders components nested inside of them, and so on until it reaches the end of the chain of components. it is recursive in Nature.
If you try to visualise a Typical React App as a Tree, every thing down from where the state update occurred will be rendered as well.
If you take a closer look at our App everything is rendering, which means those slow components are also part of it, when state changes
const App = () => {
const [isOpen, setIsOpen] = useState(false);
// everything that is returned here will be re-rendered when the state is updated”
“return (
<div className="layout">
<Button onClick={() => setIsOpen(true)}>
Place Order
</Button>
{isOpen ? (
<ModalDialog onClose={() =>
setIsOpen(false)} />
) : null}
<VerySlowComponent />
<BunchOfStuff />
<OtherStuffAlsoComplicated />
</div>
);
};
As a result, the modal is not opening instantly, as React has to render everything before the our dialog can appear.
Remember, React will never Look up the Render Tree when the re-renders occurs. if the state was triggered somewhere in the middle, only components below it would go though re-render.
The only way for components at the bottom to affect the top is either to call the state update in those components or passing components as functions.
Bursting the re-renders Myth
We have Props in React, and you might have encountered the phrase “Component re-renders when its prop change”. it’s a popular misconceptions in React amongst a lot of people, many don’t doubt it and this is not true.
Normal React behavior is that if a state update is triggered, React will re-render all the nested components regardless of their props. And if a state update is not triggered, then changing props will be just "swallowed": React doesn't monitor them.”
In the context of re-renders, whether props have changed or not on a component matters only in one case: if the said component is wrapped in the React.memo higher-order component. Then, and only then, will React stop its natural chain of re-renders and first check the props. If none of the props change, then re-renders will stop there. If even one single prop changes, they will continue as usual.
Moving state down (The solution)
Now we have clarified how the whole re-rendering thing works in React, it’s time to apply the things we learned to the problem we are dealing and fix it, let have a look at the code where we use the modal state:
const App = () => {
// our state is declared here
const [isOpen, setIsOpen] = useState(false);
return (
<div className="layout">
{/* state is used here */}
<Button onClick={() => setIsOpen(true)}>
Place Order
</Button>
{/* state is used here */}
{isOpen ? (
<ModalDialog onClose={() =>
setIsOpen(false)} /> )
: null}
<VerySlowComponent />
<BunchOfStuff />
<OtherStuffAlsoComplicated />
</div>
);
};
As we can see, it's relatively isolated: we use it only on the Button component and in ModalDialog itself. The rest of the code, all those very slow components, doesn't depend on it and therefore doesn't actually need to re-render when this state changes. “It's a classic example of what is called an unnecessary re-render.”
we could have wrapped the same in React.memo() which would prevent the re-render , but it has it own sets of nuances to dealt with. There is a better and simpler way, we can just extract the components that depends on the state , along with the state itself into a smaller isolated component like below
const ButtonWithModalDialog = () => {
const [isOpen, setIsOpen] = useState(false);
// render only Button and ModalDialog here
return (
<>
<Button onClick={() => setIsOpen(true)}>
Place Order
</Button>
{isOpen ? (
<ModalDialog onClose={() =>
setIsOpen(false)} />
) : null}
</>
);
};
And then we can just add this newly created component of ours in the App component
const App = () => {
return (
<div className="layout">
{/* here it goes, component with the state inside */}
<ButtonWithModalDialog />
<VerySlowComponent />
<BunchOfStuff />
<OtherStuffAlsoComplicated />
</div>
);
};
Updated change sandbox
Now, the state update when the Button is clicked is still triggered, and some components re- render because of it. But! It only happens with components inside the ButtonWithModalDialog component. And it’s just a button and our modal that should be rendered. The rest of out app is working as good.
We just created a new piece of branch inside our render tree and moved the state downwards thus our modal is appearing instantly as it should be, so we learned to fix this performance issue that looked big from the far but was a simple fix.
Top comments (0)