Mastering React Performance: Immutability and Memoization Explained



This content originally appeared on Level Up Coding – Medium and was authored by Goldin Evgeny

Photo by Josh Calabrese on Unsplash
Before we dive into the details, it’s crucial to cover the basics.

React, Immutability & Memoization

What is React?

React is a library that takes data and renders a UI. It achieves this by comparing two copies of the virtual DOM and then applying the changes to the actual DOM. But how does React determine that something has “changed”? Understanding this is key to reasoning about data in a way that optimizes application performance through proper data structuring.

Immutability and React

Immutability is a core concept in React. By treating props and state as immutable, React can efficiently determine when to re-render components, improving performance. Let’s explore how this works and how proper data structuring can further enhance performance.

Memoization in React

We are all familiar with the useMemo and useCallback hooks, which became obsolete with the new compiler (Check out the RC Version). Let's break down how they work and how they relate to how React considers something as "changed."

How Memoization Works

Memoization leverages the closure mechanism, or the scoping mechanism of JavaScript. Consider the following example of an “expensive” calculation:

const expensive = (a, b) => {
console.log(a, b);
return a + b;
}

const memoized = () => {
const cache = {};
return (a, b) => {
const key = `${a}.${b}`;
if (!cache[key]) {
cache[key] = expensive(a, b);
}
return cache[key];
}
}

const expensive2 = memoized();

expensive2(2, 3);
expensive2(2, 3);

In the example above, the result is not recalculated on the second call but is returned from the cache. This works well with primitive types that can be easily compared. However, with complex objects, the memoized function returns the same reference it created the first time it invoked the calculation.

React.memo and Immutability

Memoization in React is not standalone; it is coupled with React.memo. By creating a higher-order component that receives a component and a comparison algorithm, React can efficiently manage re-renders.

Usually, no one uses the second argument of React.memo, and under the hood, React uses shallow comparison as default.

Example: A Simple Todo App

Let’s look at a simple Todo app written in plain React and Redux Toolkit. This app allows users to check and uncheck todos, add new records, and delete todos.

The straightforward approach would be to fetch the data from the backend as an array and iterate over it in the list, passing each todo object to a list item component.

export const selectTodoList = (state: RootState) => state.todo.todoList;

// And then map it in the component
{_.map(todoList, (todo: TodoModal) => (
<TodoItem key={todo.id} todo={todo} />
))}

When a user checks a todo as done, even though we use our data in the reducers as mutable, under the hood Redux Toolkit uses Immer to create a new instance of the object and the array.
If you open the project above in a new tab, set ‘Highlight updates when components render’ configuration in the React DevTools to true

and check/uncheck an item, you’ll notice the entire list re-renders because a new array reference is created each time.

Solving the re-rendering issue

You might think memoization can solve this, but React.memo around TodoItem doesn't help because React uses shallow compare, which doesn't work with new array references. Instead, we can use an array of primitives (e.g., ids of the objects):

export const selectTodoIDList = (state: RootState) =>
_.map(state.todo.todoList, (todo) => todo.id);

const todoList = useAppSelector(selectTodoIDList);

{_.map(todoList, (todoId: number) => (
<TodoItem key={todo} todoId={todoId} />
))}

And now we can on the individual level have a selector for the specific todo we are interested in:

const todo = useAppSelector((state) =>
state.todo.todoList.find((todo) => todo.id === todoId)
);

A selector is just a function that takes the state and returns a value, and it is memoized.

Final Touch

Use shallowEqual from react-redux to ensure shallow comparison:

import { shallowEqual } from 'react-redux';
const todoList = useAppSelector(selectTodoIDList, shallowEqual);

Now, only the component that was clicked will re-render.

Practical Applications

This principle can be applied to various scenarios. For example, if you have a canvas of draggable items, following the principles described here will solve performance issues.

More Examples

Social Media Feeds: In a social media app, where users have a feed of posts that can be liked, commented on, or shared. By storing post IDs and using selectors to fetch post details, the app can optimize performance by re-rendering only the posts that are interacted with, rather than the entire feed.

Interactive Dashboards: For dashboards displaying real-time data, such as stock prices or sensor readings. Structuring the data in a way that only the widgets displaying updated data re-render can significantly improve performance. Using memoization and selectors to manage data updates ensures efficient re-renders.

Form Handling: In complex forms with multiple controlled inputs and sections. Using IDs to track changes to individual inputs can help avoid re-rendering the entire form on each input change, enhancing performance and user experience.

Personal Experience

In my experience, understanding these concepts is crucial. I was once asked for help where the developer had many draggable windows, and each time he moved one, all the others would re-render. By applying the principles discussed here, we managed to significantly improve the performance of the application.

References

Conclusion

Grasping and applying these principles can notably enhance the performance of your React applications. These concepts are not just about optimization; they reflect a deeper understanding of React’s inner workings. It’s a valuable test for senior developers to ensure they truly comprehend React. From my experience interviewing developers, I’ve noticed that those who are well-versed in these topics stand out in their discussions. Mastering these fundamentals will elevate your skills and help you build more efficient applications. Happy coding!


Mastering React Performance: Immutability and Memoization Explained was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding – Medium and was authored by Goldin Evgeny