Jotai’s splitAtom: Minimize Unnecessary React Re-Renders.

sabin shrestha
readytowork, Inc.
Published in
7 min readMar 23, 2024
Banner image

Jotai adopts an atomic approach for global state management in React applications. It combines atoms to construct a state, automatically optimizing renders based on atom dependencies. This eliminates unnecessary re-renders, providing a signal-like development experience within a declarative programming model.

Jotai, a powerful global state management library for React, offers various features to streamline state handling. While the primary focus of this article is on splitAtom, Jotai's capabilities extend beyond, providing a comprehensive solution for state management in React applications. If you're new to Jotai or want to explore its broader features, be sure to check out other dedicated article on the subject.

Optimizing React Rendering with splitAtom

In this article, we explore Jotai’s splitAtom utility to address and resolve unnecessary re-rendering in React components. We'll transition from the traditional useState approach to demonstrate how splitAtom significantly improves performance, particularly in scenarios involving a large number of components.

Application Overview: Cards with Interactivity

Our practical example involves a list of cards, each featuring a “like” button for user interaction. Initially managed with useState, we'll showcase the performance gains achieved by implementing splitAtom in larger applications.

Let’s Begin: React’s useState Setup

Starting with React’s native useState, we'll establish a baseline for state management. This sets the stage for comparing and optimizing our approach using Jotai's splitAtom utility.

Let’s take a look at the folder structure for this demonstration.

Folder structure for this demonstration
Folder structure for this demonstration

Lets take a look at the file CardListWithState.tsx :

// /containers/CardListWithState.tsx

import { Card } from "@/types";
import React, { useState } from "react";
import { initCards } from "@/constants";
import CardComp from "../components/CardComp";
const CardListWithState = () => {
// The initCards data is imported from a constants file
const [cards, setCards] = useState<Card[]>(initCards);
const handleLikeToggle = (id: string) => {
const newCards = cards.map((card) =>
card.id === id ? { ...card, liked: !card.liked } : card
);
setCards(newCards);
};
return (
<div>
<h1 className="text-3xl text-slate-700 mb-5">
useState (Regular React Hook)
</h1>
<div className="flex gap-8 flex-wrap">
{cards.map((card) => (
<CardComp
state={card}
key={card.id}
handleLikeToggle={handleLikeToggle}
/>
))}
</div>
</div>
);
};
export default CardListWithState;

Here we are simply storing the sample data for cards in a state and mapping it over another component called CardComp.

Type for card details:

// /types.ts

// Type for card details.
export type Card = {
id: string;
image: string;
title: string;
description: string;
liked: boolean;
};

Sample data to Map over CardComp component.

// sample data to map over card component.
import { Card } from "./types";

// const
export const initCards: Card[] = [
{
id: "1",
title: "Lorem ipsum dolor sit amet ",
description:
"Lorem ipsum dolor sit amet . Iste, reprehenderit. Lorem, ipsum dolor sit amet consectetur adipisicing elit. Neque quam delectus ipsum velit ex dolorem vero maiores nostrum impedit. Ducimus.",
image:
"<https://images.pexels.com/photos/386009/pexels-photo-386009.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2>",
liked: false,
},
{
id: "2",
title: "Lorem ipsum dolor sit amet ",
description:
"Lorem ipsum dolor sit amet . Iste, reprehenderit. Lorem, ipsum dolor sit amet consectetur adipisicing elit. Neque quam delectus ipsum velit ex dolorem vero maiores nostrum impedit. Ducimus.",
image:
"<https://images.pexels.com/photos/307008/pexels-photo-307008.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2>",
liked: true,
},

// .... so on
];

And this is the code for CardComp component:

// /components/CardComp/index.tsx

import React from "react";
import { Card } from "@/types";
import { IconHeart, IconHeartFilled } from "@tabler/icons-react";
const CardComp = ({
state,
handleLikeToggle,
}: {
state: Card;
handleLikeToggle: (id: string) => void;
}) => {
return (
<div className="w-full max-w-56 border rounded-lg shadow bg-gray-100 ">
<a href="#">
<img
className="p-4 rounded-t-lg h-[150px] w-full object-cover"
src={state?.image}
alt="product image"
/>
</a>
<div className="px-4 pb-5">
<a href="#">
<h5 className="text-xl font-semibold tracking-tight text-gray-800 mb-3">
{state?.title}
</h5>
</a>
<div className="flex items-center justify-between">
<span className="text-3xl font-bold text-slate-800 ">$599</span>
<button
className="text-red-400"
onClick={() => {
handleLikeToggle(state.id);
}}
>
{state?.liked ? (
<IconHeartFilled color="ff837b" />
) : (
<IconHeart stroke={2} />
)}
</button>
</div>
</div>
</div>
);
};
export default CardComp;

The code is not too complicated, its pretty straight forward so I won’t explain the code.

useState (regular react hook)

As you can see, we have our cards that can be liked or unliked. However, there is one problem: upon liking or unliking a card, every single card item gets re-rendered, as indicated by the highlighted border around the cards. While this may not seem problematic for a small application like this, in larger applications, it can significantly degrade performance.

Addressing Unnecessary Re-renders with splitAtom

Next, we’ll implement Jotai’s splitAtom utility to optimize our React renders. This utility will help us to split our global atom state into individual atoms for each card. This way, liking or unliking a card only triggers a re-render of that specific card, not the entire list, significantly improving rendering performance.

Similar to our application CardListWithState.tsx we will use Sample data to map over the card component. But this time as you can see we have stored the sample data inside splitAtom this splits individual items into individual atoms for each card.

Lets take a look at the file CardListWithSplitAtom.tsx.

// /containers/CardListWithSplitAtom.tsx

import React from "react";
import { Card } from "@/types";
import { initCards } from "@/constants";
import { splitAtom } from "jotai/utils";
import { atom, useAtomValue } from "jotai";
import CardCompWithAtom from "@/components/CardCompWithAtom";
const cardsAtom = atom<Card[]>(initCards);
const splitCardsAtom = splitAtom(cardsAtom);
const CardListWithSplitAtom = () => {
const cards = useAtomValue(splitCardsAtom);
return (
<div>
<h1 className="text-3xl text-slate-700 mb-5">
Jotai (split atom version)
</h1>
<div className="flex gap-8 flex-wrap ">
{cards.map((card) => (
<CardCompWithAtom atom={card} key={card.toString()} />
))}
</div>
</div>
);
};
export default CardListWithSplitAtom;

Instead of CardComp we are mapping the cards atom value over CardCompWithAtom component that is created specially to handle individual card atoms. This enables us to manage state at the granular level, eliminating unnecessary re-renders in other parts of our application.

Let’s explore the implementation of CardCompWithAtom:

// components/CardCompWithAtom.tsx
import React from "react";
import { Card } from "@/types";
import { PrimitiveAtom, useAtom } from "jotai";
import { IconHeart, IconHeartFilled } from "@tabler/icons-react";
const CardCompWithAtom = ({ atom }: { atom: PrimitiveAtom<Card> }) => {
// The useAtom hook returns an array with two elements.
// The first element is the current state of the atom, and the second is a setter function.
const [cardInfo, setCardInfo] = useAtom(atom);
return (
<div className="w-full max-w-56 border rounded-lg shadow bg-gray-100 ">
<a href="#">
<img
className="p-4 rounded-t-lg h-[150px] w-full object-cover"
src={cardInfo?.image}
alt="product image"
/>
</a>
<div className="px-4 pb-5">
<a href="#">
<h5 className="text-xl font-semibold tracking-tight text-gray-800 mb-3">
{cardInfo?.title}
</h5>
</a>
<div className="flex items-center justify-between">
<span className="text-3xl font-bold text-slate-800 ">$599</span>
{/* The button click triggers the setCardInfo function, updating the liked property of the card. */}
<button
className="text-red-400"
onClick={() => {
setCardInfo((prev) => ({
// The updater function toggles the liked property.
...prev,
liked: !prev.liked,
}));
}}
>
{cardInfo?.liked ? (
<IconHeartFilled color="ff837b" />
) : (
<IconHeart stroke={2} />
)}
</button>
</div>
</div>
</div>
);
};
export default CardCompWithAtom;

In CardCompWithAtom, the useAtom hook returns an array of two elements (this is very similar to react’s native useState hook.): the current state of the atom and a setter function. The setter function allows us to update the state of the atom. When we pass an updater function to the setter, it receives the current state of the atom and returns the new state. In this case, we're using an updater function to toggle the liked property of the card.

Jotai (split atom version)

As you can see, with splitAtom, only the card that is clicked gets re-rendered, as indicated by the highlighted border around the specific card. This significantly improves rendering performance, especially in larger applications with numerous components. Thus, by using Jotai's splitAtom utility, we can effectively manage state at a granular level and prevent unnecessary re-renders in our React applications.

Conclusion:

In this article, we delved into Jotai’s splitAtom utility, learning how it can help us optimize rendering performance in our React applications. By splitting our global atom state into individual atoms for each card, we were able to minimize unnecessary re-renders and boost application performance. Whether you're building a small project or a large-scale application, Jotai's atomic state management approach offers a powerful tool to enhance efficiency and user experience.

Happy coding…

Github repo link:

https://github.com/sabin411/jotai-split-atom

References

  1. https://jotai.org/
  2. https://jotai.org/docs/utilities/split

--

--