React Function Call vs Render
Photo By Lalit Kumar
React's rendering behavior can sometimes be surprising, especially when dealing with different data types.
Recently, I was working on a filter component with multiple select/dropdown components, where I observed a behavior that I found interesting. The gist of it is so simple, yet it was not obvious to me. I found myself testing the code wrongly for multiple times and I expected some magic ✨ to happen. Finally, I understood the root cause of it.
In this article, we will explore how React renders components when state changes. The important part is the data type of the state. Understanding this behavior can help us write more efficient and predictable code.
The Mental Model#
Before we dive in, let's establish a key concept:
- A function call is just executing the component function
- A render is when React actually updates the DOM
String vs Object State#
Let's look at two seemingly similar components that behave differently
First, let's look at the string state component
Example 1: String State#
In this example, we have appliedUser
as a string state.
1function SampleComponentString() {2 const [appliedUser, setAppliedUser] = React.useState("John");34 const handleChangeUser = (newUser) => {5 console.log("Are strings same", appliedUser === newUser);6 setAppliedUser(newUser);7 };89 return (10 <div >11 <h2 >String State</h2>12 <UserDisplayString user={appliedUser} />13 <SetUserButtonString handleChangeUser={handleChangeUser} />14 </div>15 );16}1718function UserDisplayString({ user }) {19 return <div className="p-2">User: {user}</div>;20}2122function SetUserButtonString({ handleChangeUser }) {23 const [user, setUser] = React.useState("");24 return (25 <div >26 <label htmlFor="userNameString">Update user to: </label>27 <input28 id="userNameString"29 type="text"30 value={user}31 onChange={(e) => setUser(e.target.value)}32 className="text-black"33 />34 <button35 onClick={() => handleChangeUser(user)}36 >37 Change User38 </button>39 </div>40 );41}
(Skipping the explanation of component structure for brevity)
Inside the SetUserButtonString
component, we have a state user
that is used to update the appliedUser
state.
When we change the user
state, react re-renders the SetUserButtonString
component. This is expected behavior.
We have initialized the appliedUser
state to "John". When we enter the name "John" and click the change user button, nothing happens. React does not re-render the component because it knows that the appliedUser
state is not updated. We will get the console log as Are strings same true
.
Example 2: Object State#
In this example, we have appliedUser
as an object state with a name property.
1function SampleComponentObj() {2 const [appliedUser, setAppliedUser] = React.useState({ name: "John" });34 const handleChangeUser = (newUser) => {5 console.log("Are objects same", Object.is(appliedUser, newUser));6 setAppliedUser(newUser);7 };89 return (10 <div>11 <h2 >Object State</h2>12 <UserDisplay user={appliedUser} />13 <SetUserButton handleChangeUser={handleChangeUser} />14 </div>15 );16}1718function UserDisplay({ user }) {19 return <div className="p-2">User: {user.name}</div>;20}2122function SetUserButton({ handleChangeUser }) {23 const [user, setUser] = React.useState({ name: "" });24 return (25 <div >26 <label htmlFor="userNameObj">Update user to: </label>27 <input28 id="userNameObj"29 type="text"30 value={user.name}31 onChange={(e) => setUser({ name: e.target.value })}32 className="text-black"33 />34 <button35 onClick={() => handleChangeUser(user)}36 >37 Change User38 </button>39 </div>40 );41}
This is pretty much the same as the string state component except that we are using an object.
Here, when we input the name as "John" and click the change user button, react re-renders the SampleComponentObj
component. This is because as per Javascript, { name: "John" }
and { name: "John" }
are not the same values. We will get the console log as Are objects same false
.
A quick comparison of objects in Javascript
const obj1 = { name: "John" };const obj2 = { name: "John" };console.log(obj1 === obj2); // falseconsole.log(obj1 == obj2); // falseconsole.log(Object.is(obj1, obj2)); // falseconsole.log(JSON.stringify(obj1) === JSON.stringify(obj2)); // true
In the above example, ===
and ==
both return false because they are checking for reference equality. JSON.stringify
returns true because it is checking for value equality. React uses Object.is
to check for equality.
Since, the equality fails, react re-renders the SampleComponentObj
component.
This is good, but what if we click the change user button again? In both the components?
Clicking the button again#
In the String component, no matter how many times we click the button, react does not re render the component. This is because react knows that the appliedUser
state is not changing.
const str1 = "John";const str2 = "John";console.log(Object.is(str1, str2)); // true
Even in the Object component, after the first click, react does not re render the component. How do we verify this?
Let's add a console log in both the components.
1function SampleComponentString() {2 const [appliedUser, setAppliedUser] = React.useState("John");34 console.log("string component rerender");56 return (/* Omitted for brevity */);7}89function SampleComponentObj() {10 const [appliedUser, setAppliedUser] = React.useState({ name: "John" });1112 console.log("object component rerender");1314 return (/* Omitted for brevity */);15}
In the examples below, please ignore the first render with initial state.
The output of string component is
No surprise here. Once react knows that the state is not changing, it does not re-render the component. So, our console log is not printed.
The output of object component is
(Please ignore the first render with initial state.)
We are seeing a weird behavior. It looks like the component is rendered twice.
- When user updates the state to "John", and clicks the button, we are seeing "object component rerender".
- When user clicks the button again, we are seeing "object component rerender" again.
- Next time when user clicks the button, we are not seeing "object component rerender"?
If our understanding is correct, the component should not rerender once the objects become same.
Can you guess why this is happening? 🤔
Actually, our understanding is correct. There is a mistake in the way we are checking if react renders the component or not.
React rendering and function calls are not the same. React can sometimes skip the rendering phase if it can determine that the component is not changing. In these cases, the function call will be made but the rendering phase will be skipped. This is what is happening in the object component.
The proper way to check if react renders the component is by using useEffect
hook.
1function SampleComponentObj() {2 const [appliedUser, setAppliedUser] = React.useState({ name: "John" });34 React.useEffect(() => {5 console.log("object component rerender");6 });78 return (/* Omitted for brevity */);9}
Now, let's see this in action.
(Please ignore the first render with initial state.)
- When user clicks the button, we are seeing "object component rerender".
- When user clicks the button again, we are not seeing "object component rerender". Here react calls the component function but skips the rendering phase.
React Dev Tools#
React Dev Tools is a powerful tool that can help you understand how React renders the component. Let's see how it works with our examples. In both of the examples, after entering the name as "John" I am clicking the button multiple times.
String Component#
Total number of commits are 5.
- Initial render.
SetUserButtonString
component is rendered for "J".SetUserButtonString
component is rendered for "o".SetUserButtonString
component is rendered for "h".SetUserButtonString
component is rendered for "n".
Object Component#
Total number of commits are 6.
- Initial render.
SetUserButton
component is rendered for "J".SetUserButton
component is rendered for "o".SetUserButton
component is rendered for "h".SetUserButton
component is rendered for "n".SampleComponentObj
component is rendered for{name: "John"}
.
Playground#
You can play with the code here - Github Repo
Conclusion#
React rendering and function calls are not the same. React can sometimes skip the rendering phase if it can determine that the component is not changing. In these cases, the function call will be made but the rendering phase will be skipped. This is what is happening in the object component.
In summary,
The quicker way to check if react renders the component is by using
useEffect
hook.
The better way to check if react renders the component is by using React Dev Tools.
The next time when you are debugging a React component's rendering behavior, use the strategies mentioned above. Remember that:
- Console logs can be misleading since function calls don't always mean renders
useEffect
is a quick way to check for actual renders- React Dev Tools provides the most accurate picture of component rendering
- Understanding how React handles equality checks for different data types (like strings vs objects) will help you write more predictable code
Happy debugging! 🐛
References
Published:December 8, 2024
Updated:December 14, 2024