Some Optimization Techniques in React

Omar Belghaouti
EGYM Software Development
7 min readMar 14, 2023

--

Photo by Lautaro Andreani on Unsplash

React is a famous JS library that still exists because of its ergonomic way of building web pages or even native apps with React Native. Also, it inspires new web libraries and frameworks to make better solutions around existing problems like SolidJS, NextJS, Remix, and so on.

This article is about how you would write a more performant React app with built-in hooks. Before we go any further, this article is dedicated to React developers who know at least the basics of it and how it works as we are going to compare and adjust some of the sample codes to make them perform better. Also, we will not use class components along the way, we will be using function components as it’s the simple and suggested way to build React apps.

So what is performance in React?

Behind the scenes, React does a lot of smart techniques to optimize your app without changing anything in your code. For example, we know that setting a state in React will cause the component to be re-rendered with the updated state, so taking this into consideration, what happens if you set multiple states at once?

import { FC, useEffect, useState } from "react";

const Component: FC = () => {
const [state1, setState1] = useState("");
const [state2, setState2] = useState("");
useEffect(() => {
// do something
setState1("Hey");
setState2("there");
}, []);
return <div>{state1} - {state2}</div>;
}
export default Component;

Well even if you’re setting multiple states at once, React will re-render the component once by batch-updating these states.

Not to mention a lot of optimization techniques that happen when you run the production build. So you may say “In that case, I’ll just keep writing as I usually do without thinking about performance“.

There is a lot to consider when it comes to performance optimization in React and here we’re going to see some problems that even React provided solutions for.

Call Stack

When a function gets called in JavaScript, there is a place in memory to store that call on top of the global execution context which is the “Call stack“, this is a mechanism to keep track of the function calls. So when you do something like this:

import { FC } from "react";

const Component: FC = () => {
function onClick() {
console.log("clicked!");
}
return <button onClick={onClick}>Click me</button>
}
export default Component;

What will happen here is that each time you click the button, the function call will be stacked on top of the previous calls thus a lot of work in memory is going on!

If we call the same function that does the same thing each time for the same arguments, then it’s not optimal to add the called function each time. A solution for this is provided by React which is the useCallback hook.

For the previous example, we can wrap the onClick function with this hook as follows:

import { FC, useCallback } from "react";

const Component: FC = () => {
const onClick = useCallback(() => {
console.log("clicked!");
}, []);
return <button onClick={onClick}>Click me</button>
}
export default Component;

So what happens is that at first, the called function will be pushed to the call stack, but not subsequently, meaning we saved some workload for the CPU by just adding this hook, cool right?

As you can see theuseCallback hook accepts 2 arguments, the first one is the callback and the second is the dependency list (which is exactly the same as in the useEffect hook), this is helpful if there is any change for your states the function will be called for the provided state and caches the results between each render, here is an example to see how it works:

import { FC, useCallback, useState } from "react";

const Component: FC = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = useCallback(() => {
// do your thing 😉
}, [username, password]);
return (
<form>
{/* ...... */}
<button onClick={handleSubmit}>Submit</button>
</form>
);
}
export default Component;

In this example, the handleSubmit will get called at the first render, and cache the call for the provided states (username and password). Now at first, they’re empty but whenever there is a change for at least one of them, and when the button is clicked the function will get called again with the new provided states and cache this call for this change, in this example the user may click the submit button so many times for no reason, or a cute cat jumps into the keyboard like this one 😍

Memoization

Memoization is one of the software techniques that should be considered when dealing with optimization, it occurs a lot in our daily lives and not only in React.

Let’s take this factorial function for a second:

const factorial = (n: number) => {
let res = 1;
for (let i = 1; i <= n; i++) {
res *= i;
}
return res;
}

What it does is it calculates the factorial of a given number and returns the result. When we call it each time, it will recalculate the result by looping again and again, but what if we remember the result of the last given number so if it’s called again with the same arguments it won’t go through the loop again? Yes, we can!

const factorial = (n: number, cache: Record<number, number> = {}) => {
if (cache[n]) return cache[n]; // return directly from the cache
let res = 1;
for (let i = 1; i <= n; i++) {
res *= i;
}
cache[n] = res; // save the result for the given number
return res;
}

What happens now is that if we call the factorial function with a previously given argument, it will not recalculate the result but return back the result from the cache, this is “Memoization“ and you can say the function is memorizing the results for each newly given argument.

In React world, this is indeed valuable and needed if there is any optimization at hand. Also, there is some variety of memoization like memoizing the component or memoizing the current function.

Memoizing a component

Sometimes we separate our page for different reusable components, and by React nature, each time there is a state update the whole page will get re-rendered with all its used components. Of course, sometimes we have some fixed components that don’t need to be re-rendered if the page gets updated, for that there is this pretty function from React called memo that solves this issue, here’s an example of how it can be implemented:

import { FC, memo, useState } from "react";

const Header: FC = () => <header>This is a header</header>;
const HeaderMemoized = memo(Header);

export default function App() {
const [state, setState] = useState();
// ...
return (
<>
<HeaderMemoized />
{/* ... */}
</>
);
}

Now each time the App re-renders if there is any update, the HeaderMemoized will not get re-rendered because we are memoizing this component.

Even if the memoized component holds states from the parent component, it won’t get re-rendered only if there is a change in that state, like this:

import { FC, memo, useState } from "react";

interface HeaderProps {
state: any;
}

const Header: FC<HeaderProps> = ({state}) => <header>This is a header</header>;
const HeaderMemoized = memo(Header);

export default function App() {
const [state, setState] = useState();
// ...
return (
<>
<HeaderMemoized state={state} />
{/* ... */}
</>
);
}

So you can imagine if you have multiple states and you only need to re-render the child component if some of them are changed.

Memoizing a function

As talked previously with the factorial function, you can do something similar with the help of React library which is the useMemo hook.

You can say that memo function and useMemo hook are most of the time used together, but that doesn’t mean they should work together. This hook helps with memoizing the return value from the given function so it won’t be recalculated for the same inputs again.

Let’s say that we have a child component that has props from the parent component and you want a result to be calculated each time there is a new thing coming from the props, like this:

import { useMemo } from "react";

export default function Child({state1, state2}) {
const memoizedResult = useMemo(() => {
let result;
// do something with these state and return the result
return result;
}, [state1, state2]);
return (
// Something 🏗️ used with memoizedResult
);
}

An example of this could be when calculating the total price of the products in a point of sale (POS) when you add each time a product, like this:

import { FC, useMemo } from "react";

interface CheckoutProps {
productPrices: []number;
}

export const Checkout: FC<CheckoutProps> = ({productPrices}) => {
const totalPrice = useMemo(() => {
return productPrices.reduce((acc, curr) => acc + curr, 0)
}, [productPrices]);
return (
<div>Total price: {totalPrice}</div>
);
}

Or even when you have a filter component where you can pass filter values and apply the result of filters for the current list. There are many cases where you can use it 😉

Conclusion

These two techniques can be used to optimize your React app, of course, there are other techniques as well, but these are the most famous and you can say they are the building blocks for more complex optimizations.

--

--