State management in React, particularly for newcomers, can be confusing. A fundamental grasp of React's Virtual DOM and the nuances between the different types of JavaScript values can save you from common headaches. In this blog post, we'll delve into a seemingly simple counter-like example to figure out why React might not trigger a re-render when the state is updated with the same value and a quick solution for it.
// React Component
import React, { useState, useEffect } from 'react';
const ExampleComponent = () => {
const [state, setState] = useState(0);
const [renderCount, setRenderCount] = useState(0);
// Updating render count whenever state changes
useEffect(() => {
setRenderCount((prev) => prev + 1);
}, [state]);
const handleButtonClick = (newState) => {
setState(newState);
};
return (
<div>
<p>State: {state}</p>
<p>Render Count: {renderCount}</p>
<button onClick={() => handleButtonClick(1)}>Increment by 1</button>
<button onClick={() => handleButtonClick(2)}>Increment by 2</button>
</div>
);
};
export default ExampleComponent;
*The Head-Scratcher*
In the React component above (Check out the example here), the objective is to update the render count each time a button is clicked, irrespective of the button's repetition. While state updates correctly, an issue surfaces when the same button is clicked multiple times without switching to another button. The render count stubbornly refuses to update, regardless of the clicking speed.
Letβs Break it Down
The underlying cause lies in understanding how React optimizes performance through the reconciliation of the virtual and actual DOM, and also on the type of Javascript value. Let's talk about the types of values first.
Value Types: Primitives vs Reference Types
Understanding JavaScript value types, specifically primitives and reference types, sheds light on this behaviour. Consider the code below:
//Example: 1
console.log("myName" === "myName") // returns true
//Example: 2
console.log({name: "daniel"} === {name: "daniel"}) // returns false
String comparison (primitives) returns true
, but object comparison (reference types) returns false
.
Primitives
Primitives such as numbers and strings, are data types which store their values directly in memory. React treats primitives straightforwardly. If the new state differs from the old one for primitives, React re-renders the component, else there would be no re-rendering and everything stays the same. And this is really important to remember.
Reference Types
Reference types, such as objects, arrays, and dates, do not directly contain the data. Instead, they store a reference or address pointing to the memory location where the actual data or object is stored (often referred to as the memory heap).
When comparing two objects in JavaScript, it's not the values or properties that are compared, but rather the addresses pointing to their respective locations in memory. In the provided example, the two objects are distinct because they are created separately using object literals ({}
) and would have different addresses which returns a false
value when compared.
During state comparison in React for reference types, the framework checks whether the memory addresses of the old state and new state match. If they match, React assumes that the state hasn't changed, avoiding a re-render. Conversely, if the addresses differ, React initiates a re-render.
It's important to note that there are various ways to create reference types or objects, but in this context, our focus is on objects created through object literals ({}
).
Actual and Virtual DOM in React
In simple terms, the actual DOM represents the real-time structure of a webpage, while the virtual DOM is a streamlined copy that React uses for efficient updates, minimizing changes to the actual DOM to enhance performance.
React's efficiency lies in its ability to selectively update only what's necessary. When using setState
to update a component's state, React compares the previous state value with the new one. If they differ, a re-render occurs. This explains why our component isn't re-rendering.
However, it's crucial for our app to maintain an up-to-date render count. While better solutions exist, like introducing an additional state to track the count when the button is clicked, for the sake of learning and applying knowledge, we'll use what we've learned about JavaScript value types to address this bug.
*The Fix*
Consider transforming the state value into an object with a count property ({count: 0}
). This strategy guarantees that every setState
call produces a new object in memory, each with its distinct address. This distinction enables React to identify updates as distinct entities, triggering a re-render. Consequently, when attempting comparisons before updating the DOM, the previous state and new state will consistently differ. And this works because
// Updated React Component
import React, { useState, useEffect } from 'react';
const CounterComponent = () => {
const [state, setState] = useState({ count: 0 });
const [renderCount, setRenderCount] = useState(0);
// Updating render count whenever state changes
useEffect(() => {
setRenderCount((prev) => prev + 1);
}, [state]);
const handleButtonClick = (newState) => {
setState({ count: newState });
};
return (
<div>
<p>State: {state.count}</p>
<p>Render Count: {renderCount}</p>
<button onClick={() => handleButtonClick(1)}>Increment by 1</button>
<button onClick={() => handleButtonClick(2)}>Increment by 2</button>
</div>
);
};
export default CounterComponent;
*Wrapping it Up*
A solid understanding of React's rendering principles, particularly the interplay between value types, empowers you to craft efficient React components. This insight ensures that state changes result in the expected re-rendering behavior, enhancing the performance of your React applications.
Top comments (3)
I mean... Yes... And great explanation... But breaking the diffing like this, is something you DON'T want to do in real applications. You want diffing to only detect changes when the values in the state actually change and something else needs to be shown to the user.
Thank you for your comment! I completely agree with you. The approach of creating a new object with each setState call, as mentioned in the blog, is more of an illustrative example to emphasize the underlying principles.
In which scenarios would I need to re-render when the actual value haven't changed?