Written by Kasra Khosravi ✏️
Over the last few years, state management in React has undergone a major evolution. With solutions like the built-in React Context API and React Redux, it has never been easier to keep a global state and track changes. However, one persisting challenge when implementing these tools is optimizing performance and preventing components from unnecessarily re-rendering.
While in a smaller application, excessive re-rendering may be unnoticeable or have no negative impact, as your application grows, each re-render may cause delays or lags in your UI. In this tutorial, we’ll use React Tracked, a library for state usage tracking, to optimize our application’s performance by preventing unnecessary re-renders.
Installing React Tracked
To get started, set up a new React project on your machine. Open the project in the terminal and add the following command to install the React Tracked library:
yarn add react-tracked scheduler
Now, let’s clean up our project by giving it the following structure:
Setting up our application
Let’s compare React Tracked with the vanilla React implementation of a shared state. We’ll create a simple global context that has two counter components, each using one value.
Add the following code in App.js
:
import Counter1 from "./Counter1";
import Counter2 from "./Counter2";
import { SharedStateProvider } from "./store";
function App() {
return (
<>
<SharedStateProvider>
<div
style={{
display: "flex",
flexDirection: "row",
border: "1px solid black",
justifyContent: "space-around",
}}
>
<Counter1 />
<Counter2 />
</div>
</SharedStateProvider>
</>
);
}
export default App;
To create the counter components, add the following code in each file:
Counter1
import React from "react";
import { useSharedState } from "./store";
export default function Counter1() {
const [state, setState] = useSharedState();
const increment = () => {
setState((prev) => ({ ...prev, count1: prev.count1 + 1 }));
};
return (
<div>
{state.count1}
{console.log("render counter 1")}
<button onClick={increment}>Increment count1</button>
</div>
);
}
Counter2
import React from "react";
import { useSharedState } from "./store";
export default function Counter2() {
const [state, setState] = useSharedState();
const increment = () => {
setState((prev) => ({ ...prev, count2: prev.count2 + 1 }));
};
return (
<div>
{state.count1}
{console.log("render counter 2")}
<button onClick={increment}>Increment count2</button>
</div>
);
}
store.js
Lastly, let’s create our store.js
file, which uses the global counter context and the useSharedState()
Hook for the states in the counter component:
import React, { createContext, useState, useContext } from "react";
const initialState = {
count1: 0,
count2: 0,
};
const useValue = () => useState(initialState);
const MyContext = createContext(null);
export const useSharedState = () => {
const value = useContext(MyContext);
return value;
};
export const SharedStateProvider = ({ children }) => (
<MyContext.Provider value={useValue()}>{children}</MyContext.Provider>
);
To run the project, add the following command:
yarn start
Now, we’ll see the following output on the browser screen:
Open the browser console and hit each Increment button three times. We’ll receive the following output:
Each component re-rendered regardless of whether the state was updated. Ideally, the component should re-render only when the state is changed.
In our example, there should have been a total of six re-renders, three for both components, however, we wound up with 12, indicating that both components re-rendered on each click.
Rendering a large list
Now, let’s try rendering a large list of elements. Add the code below to both Counter1
and Counter2
to generate a list of 10,000 random numbers in each component:
import React, { useEffect, useState } from "react";
import { useSharedState } from "./store";
export default function Counter1() {
const [state, setState] = useSharedState();
const [randomNumbers, setRandomNumbers] = useState([]);
const increment = () => {
setState((prev) => ({ ...prev, count1: prev.count1 + 1 }));
};
const generateHugeList = () => {
let list = [];
for (let i = 0; i < 10000; i++) {
list.push(Math.floor(Math.random() * 10));
}
setRandomNumbers(list);
};
useEffect(() => {
generateHugeList();
}, []);
return (
<div>
{state.count1}
{console.log("render counter 1")}
<button onClick={increment}>Increment count1</button>
{randomNumbers.map((number) => {
return <p>{number}</p>;
})}
</div>
);
}
The counter components render the list on the browser, producing an output similar to the following:
With the introduction of these new elements, our application requires more time to load:
On the first load, CPU usage jumps to 100 percent:
React will paint all the elements to the browser DOM on the first render, so 100 percent CPU usage is typical. However, after clicking the Increment button on each counter component, the CPU usage remains at 100 percent, indicating that both counters are re-rendered constantly:
Options for preventing re-renders
One popular method for preventing re-renders is using Selectors in React Redux, which are functions that subscribe to the Redux store and run whenever an action is dispatched. Selectors use ===
as a strict quality check, re-rendering the component whenever data is changed. While this process works well for variables, functions, which return a new reference each time the data is changed, are re-rendered constantly.
On the other hand, React Tracked wraps the context
object and returns its own provider by using JavaScript proxies to track changes to the individual attribute of the state.
Proxies wrap a single object, intercepting or changing its fundamental operations. React Tracked implements proxies that examine the state inside of a component, re-rendering it only if the information changes. To see proxies in action, let's implement React Tracked in our application.
Rendering a list with React Tracked
First, we need to modify the store.js
file that we created earlier by adding the following code:
import { useState } from "react";
import { createContainer } from "react-tracked";
const initialState = {
count1: 0,
count2: 0,
};
const useMyState = () => useState(initialState);
export const { Provider: SharedStateProvider, useTracked: useSharedState } =
createContainer(useMyState);
In the code above, we import createContainer()
, which returns a React Tracked provider. The useTracked
Hook creates a proxy for our state.
Now, let’s rebuild the project and compare the output from earlier to the output with React Tracked:
As an example, when we select the Increment count1 button, on the first render, both Counter1
and Counter2
are rendered. However, on subsequent clicks, only Counter1
is re-rendered, reducing CPU usage overall and improving our app’s performance.
Conclusion
In this tutorial, we explored the unwanted performance drawbacks that are caused by unnecessary re-rendering. While tools like React Redux and the React Context API make it easy to track changes in your application's state, they do not provide a straightforward solution to minimizing re-renders.
Using the React Tracked library, we built an application and minimized the number of times our counter components were re-rendered, decreasing the usage of our CPU and improving performance overall. I hope you enjoyed this tutorial!
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Top comments (0)