React rendering
Concepts of React Renderingโ
Rendering is the process of constructing the DOM tree, creating the frames, and showing it on the screen. Generally, it consists of multiple steps known as the Critical Rendering Path
(CRP).
CRP is the sequence of steps the browser goes through to convert the HTML, CSS, and JavaScript into pixels on the screen. Optimizing the CRP is important to repaints can happen at 60 frames per second.
One way to optimize the CRP is to reduce the workload on the main thread. As we know every time React detects a change in a component, it tries to re-render the component. It follows the below steps:
-
Render: A common misconception for React beginners is that
render
means updating the pixels in the browser screen, which is not correct. In React,render
means converting the JSX to a bunch ofReact.createElement()
statements and forms a new DOM. -
Reconciliation: In this step, React compares the existing DOM, with the new DOM and figures out the difference.
-
Commit: After the reconciliation phase, React knows the changes, it surgically updates the virtual DOM and then commits it to the actual DOM.
After the commit
phase, the browser does the layout calculation and updates the frame. Render and Reconciliation phases could take a substantial amount of time based on the number of changes, complexity, and size of the app. As developers, we should have a keen eye on the number of re-renders happening and understand the reason behind it. So that when the time comes, it will help us to make better design decisions. In this article, we will discuss how React re-renders new frames and ways to optimize them.
Frame and Frame rate ๐โ
The frame is a slice of the screen at any given time. Let's visualize a flipbook to understand this concept.
In a flipbook, each page is a frame and it changes multiple times in a second. Changing the page can be compared to the render tree
phase and showing it can be compared to the paint
phase.
Normally a monitor refreshes at 60hz (i.e. 60 frames/ second).
60hz = 60 frames / 1 seconds
If we divide 1 second into 60 equal parts, we get ~16.66ms / frame.
If the browser takes ~6ms to finish its routine tasks then React has to finish the render + reconciliation + commit phase
within ~10ms. But these phases can take quite some time based on the complexity of the application which can lead to frame drops sometimes. React handles the major optimization part with its new fiber architecture.
For more in-depth details on React fiber please check out these articles
But as developers, sometimes we end up writing codes that don't perform as expected. There the knowledge of How React handles rendering
comes in handy and we know exactly what is happening to our code.
How does React know that it needs to render again?โ
Component state changesโ
In React components, we create states using useState
hook, which returns a value and a setter method. When the setter method is called with a new value, React flags that corresponding component to re-render.
function Component() {
console.log('component rendered !!');
const [counter, setCounter] = useState(0);
return (
<div>
<p>value: {counter}</p>
<button onClick={() => setCounter(counter + 1)}>ADD</button>
</div>
);
}
On button click, we should expect this component to re-render due to state change.
console
component rendered !!
Simple right !! Now let's see what happens here.
function Component() {
console.log('component rendered !!');
const [counter, setCounter] = useState(0);
const [text, setText] = useState('');
const updateState = () => {
setCounter(counter + 1);
setText('updated value');
};
return (
<div>
<p>
value: {counter} {text}
</p>
<button onClick={() => updateState()}>ADD</button>
</div>
);
}
How many times the component will render, 1 or 2? Since 2 states are updated, should it be 2 ?? ๐ค.
console
component rendered !!
Umm... Ok but how ?? This is the auto batching feature of React. It batches both state updates and flags the component. Therefore component renders only once.
Now let's look into another scenario
function Component() {
console.log('component rendered !!');
const [counter, setCounter] = useState(0);
const [text, setText] = useState('');
const updateState = () => {
fetch('<SOME_URL>').then(() => {
setCounter(counter + 1);
setText('updated value');
});
};
return (
<div>
<p>
value: {counter} {text}
</p>
<button onClick={() => updateState()}>ADD</button>
</div>
);
}
console
component rendered !!
component rendered !!
What ๐คฏ?? but how?? Doesn't the auto batching work here? The answer is yes. Auto batching does not happen inside async methods. Since both the state updates are happening inside fetch.then() , it is not batched. This behavior was there till React v17. It will behave in the same way for setTimeout as well.
In v18, react supports batching for async methods inside concurrent mode.
Dispatching an action from useReducer
hookโ
useReducer is another way to maintain the state in a component. In contrast to useState, it takes 2 inputs i.e. a reducer function and an initial value. So when an action is dispatched, React flags the component to re-render.
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Component() {
console.log('component rendered !!');
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
Re-rendering Parent component re-renders Child componentโ
Whenever the components render function is called, all its subsequent child components will re-render, regardless of whether their props have changed or not.
function Parent() {
const [state, setState] = useState(0);
useEffect(() => {
setInterval(() => setState(state + 1), 1000);
}, []);
return <Child value={'Hello'} />;
}
function Child({ value }) {
console.log('Child component rendered');
return (
<>
<h1>{value}</h1>
{/* prints: 'Hello' */}
</>
);
}
console
Child component rendered
Child component rendered
Child component rendered
.
.
. (multiple times)
In the above example, Parent component
will rerender every 1s, and so is Child component
. But as we can see, the view of the Child component never changes. It is not necessary to re-render it. In large projects, it can cause performance issues.
Okay, so how to fix it ? Here React.memo comes to rescue.
const MemoizedChild = React.memo(function Child({ value }) {
console.log('Child component rendered');
return (
<>
<h1>{value}</h1>
{/* prints: 'Hello' */}
</>
);
});
function Parent() {
const [state, setState] = useState(0);
useEffect(() => {
setInterval(() => setState(state + 1), 1000);
}, []);
return <MemoizedChild value={'Hello'} />;
}
console
Child component rendered
Now React will only re-render Child component
only when it receives new/modified props.
Let's see one more scenario with React.memo
.
const MemoizedChild = React.memo(function Child({ showHello }) {
return (
<>
<h1>{showHello.value}</h1>
{/* prints: 'Hello' */}
</>
);
});
function Parent() {
const [state, setState] = useState(0);
useEffect(() => {
setInterval(() => setState(state + 1), 1000);
}, []);
const showHello = { value: 'Hello' };
return <MemoizedChild showHello={showHello} />;
}
console
Child component rendered
Child component rendered
Child component rendered
.
.
. (multiple times)
Why child component
is re-rendering multiple times? Isn't React.memo
working?
React.memo is working fine here. The issue is with Parent component
. It is creating a new reference of showHello
object every time it re-renders. Therefore, React.memo
finds a new reference and lets the Child Component
re-render again. We can fix it using useMemo
hook.
function Parent() {
const [state, setState] = useState(0);
useEffect(() => {
setInterval(() => setState(state + 1), 1000);
}, []);
const showHello = useMemo(() => ({ value: 'Hello' }));
return <MemoizedChild showHello={showHello} />;
}
Similar to useMemo
, We are having another hook called useCallback
. While useMemo
memoizes value, useCallback
memoizes methods.
Do checkout this excellent article by Kent C. Dodds on the usage of
useMemo
&useCallback
Consuming Context value from context providerโ
const UserContext = React.createContext();
const MemoizedUser = React.memo(function User() {
const value = useContext(UserContext);
return (
<>
<h1>{value.userState}</h1>
{/* prints: 'logged in' */}
</>
);
});
function App() {
const [userState, setUserState] = useState('logged in');
const value = useMemo(() => ({ userState, setUserState }), [userState]);
return (
<UserContext.Provider value={value}>
<Nav />
<MemoizedUser />
</UserContext.Provider>
);
}
function Nav() {
const value = useContext(UserContext);
return (
<button onClick={() => value.setUserState('logged Out')}>
{' '}
log out
</button>
);
}
Here, if we click log out
button in the Nav
component, User
will rerender. But this time we know that it is not happening because App
(i.e. parent component) rerendered. We have used React.memo
this time and it ensures that unless there are prop changes User
will not re-render.
Since User
is consuming the UserContext
and _there is a change in context values, React flags it to re-render _.
Now lets discuss an interesting scenario, where our component needs only a part of context value.
const AppContext = React.createContext();
const MemoizedUser = React.memo(function User() {
const { userState } = useContext(AppContext);
return (
<>
<h1>{userState}</h1>
{/* prints: 'logged in' */}
</>
);
});
function App() {
const [userState, setUserState] = useState('logged in');
const [theme, setTheme] = useState('dark');
const value = useMemo(() => ({ userState, theme }), [userState, theme]);
return (
<AppContext.Provider value={value}>
<button onClick={() => setTheme('light')}>light theme</button>
<MemoizedUser />
</AppContext.Provider>
);
}
In the above example,User
component requires only userState
from the context. But when we change the theme, the User
component gets re-rendered. We are not expecting this behavior. The solution is not as simple as previous issues. In this article, Daishi has discussed 4 ways to handle this problem.
Thank you for taking the time and reading this far. I hope you find this post useful. I will be happy to receive feedback. Please give a reaction if you liked my posts, it will encourage me to write more articles in the future. Have a great day ahead !!