React Function Call vs Render

gray and black industrial machine

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");
3
4 const handleChangeUser = (newUser) => {
5 console.log("Are strings same", appliedUser === newUser);
6 setAppliedUser(newUser);
7 };
8
9 return (
10 <div >
11 <h2 >String State</h2>
12 <UserDisplayString user={appliedUser} />
13 <SetUserButtonString handleChangeUser={handleChangeUser} />
14 </div>
15 );
16}
17
18function UserDisplayString({ user }) {
19 return <div className="p-2">User: {user}</div>;
20}
21
22function SetUserButtonString({ handleChangeUser }) {
23 const [user, setUser] = React.useState("");
24 return (
25 <div >
26 <label htmlFor="userNameString">Update user to: </label>
27 <input
28 id="userNameString"
29 type="text"
30 value={user}
31 onChange={(e) => setUser(e.target.value)}
32 className="text-black"
33 />
34 <button
35 onClick={() => handleChangeUser(user)}
36 >
37 Change User
38 </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" });
3
4 const handleChangeUser = (newUser) => {
5 console.log("Are objects same", Object.is(appliedUser, newUser));
6 setAppliedUser(newUser);
7 };
8
9 return (
10 <div>
11 <h2 >Object State</h2>
12 <UserDisplay user={appliedUser} />
13 <SetUserButton handleChangeUser={handleChangeUser} />
14 </div>
15 );
16}
17
18function UserDisplay({ user }) {
19 return <div className="p-2">User: {user.name}</div>;
20}
21
22function SetUserButton({ handleChangeUser }) {
23 const [user, setUser] = React.useState({ name: "" });
24 return (
25 <div >
26 <label htmlFor="userNameObj">Update user to: </label>
27 <input
28 id="userNameObj"
29 type="text"
30 value={user.name}
31 onChange={(e) => setUser({ name: e.target.value })}
32 className="text-black"
33 />
34 <button
35 onClick={() => handleChangeUser(user)}
36 >
37 Change User
38 </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); // false
console.log(obj1 == obj2); // false
console.log(Object.is(obj1, obj2)); // false
console.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");
3
4 console.log("string component rerender");
5
6 return (/* Omitted for brevity */);
7}
8
9function SampleComponentObj() {
10 const [appliedUser, setAppliedUser] = React.useState({ name: "John" });
11
12 console.log("object component rerender");
13
14 return (/* Omitted for brevity */);
15}

In the examples below, please ignore the first render with initial state.

The output of string component is

String Render

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

Object Render

(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" });
3
4 React.useEffect(() => {
5 console.log("object component rerender");
6 });
7
8 return (/* Omitted for brevity */);
9}

Now, let's see this in action.

Object Render Correct Way

(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#

String Component

Total number of commits are 5.

  1. Initial render.
  2. SetUserButtonString component is rendered for "J".
  3. SetUserButtonString component is rendered for "o".
  4. SetUserButtonString component is rendered for "h".
  5. SetUserButtonString component is rendered for "n".

Object Component#

Object Component

Total number of commits are 6.

  1. Initial render.
  2. SetUserButton component is rendered for "J".
  3. SetUserButton component is rendered for "o".
  4. SetUserButton component is rendered for "h".
  5. SetUserButton component is rendered for "n".
  6. 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