Async Atoms — Easy State Management in Next.js with Jotai

Raşit Çolakel
4 min readMar 27, 2024

--

Jotai also supports async atoms. Async atoms are atoms that can be read and written asynchronously. You can create an async atom using the atom function and loadable function from the jotai/utils module. You can create an async atom like this:

import { atom } from "jotai";
import { loadable } from "jotai/utils";
import { Post } from "./user-atom";
export const postsPaginationAtom = atom({
start: 0,
limit: 5,
});

const postsAtom = atom<Promise<Post[]>>(async (get) => {
// when the pagination changes, this atom will be re-evaluated
const pagination = get(postsPaginationAtom);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_start=${pagination.start}&_limit=${pagination.limit}`
);
const data = await response.json();
return data;
});

export const getPostsAtom = loadable(postsAtom);

Explanation:

  1. We create a postsPaginationAtom atom to store the pagination information.
  2. We create a postsAtom atom that fetches the posts based on the pagination information.
  3. We create a getPostsAtom atom using the loadable function, which allows us to read and write the async atom.

Now, we can use the async atom in a component like this:

import { useAtom } from "jotai";
import { getPostsAtom } from "@/atoms/posts-atom";

function PostsPage() {
const [response] = useAtom(getPostsAtom);
...
}

The response variable will have the following shape:

{
state: 'loading' | 'hasData' | 'hasError',
data?: any,
error?: any,
}

You can use the state property to render different UI based on the state of the async atom. For example:

if (response.state === "loading") {
// render loading state
} if (response.state === "hasError") {
// render error state
} if (response.state === "hasData") {
// render data state
}

The data property will contain the data if the state is hasData, and the error property will contain the error if the state is hasError. Here is an example of a component that uses the async atom:

"use client";
import { getPostsAtom } from "@/atoms/async-atom";
import { useAtom } from "jotai";
import React from "react";
import AsyncPostList from "./posts";
import Pagination from "./pagination";
type Props = {};

export default function AsyncAtomPage({}: Props) {
const [response] = useAtom(getPostsAtom);

if (response.state === "loading") {
return <div>Loading...</div>;
}
if (response.state === "hasError") {
if (response.error instanceof Error) {
return <div>{response.error.message}</div>;
} else {
return <div>Unknown error</div>;
}
}

return (
<div className="flex flex-col items-center space-y-4">
<AsyncPostList posts={response.data} />
<Pagination />
</div>
);
}

The Pagination component can be implemented like this:

import { postsPaginationAtom } from "@/atoms/async-atom";
import { useAtom } from "jotai";
import React from "react";

type Props = {};

const Pagination = (props: Props) => {
const [pagination, setPagination] = useAtom(postsPaginationAtom);

const handleNext = () => {
setPagination({ ...pagination, start: pagination.start + 1 });
};

const handlePrevious = () => {
setPagination({ ...pagination, start: pagination.start - 1 });
};

return (
<div className="flex flex-row items-center space-x-4 p-2 mb-4">
<button
onClick={handlePrevious}
disabled={pagination.start === 0}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
>
Previous
</button>
<span>Page: {pagination.start + 1}</span>
<button
onClick={handleNext}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Next
</button>
</div>
);
};

export default Pagination;

And, the AsyncPostList component can be implemented like this:

"use client";
import { Post } from "@/atoms/user-atom";
import PostCard from "../post-card";
import Header from "./header";

type Props = {
posts: Post[];
};

function AsyncPostList({ posts }: Props) {
return (
<div className="flex flex-col items-center space-y-4 max-w-2xl">
<Header />
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}

export default AsyncPostList;

The Header component is used to display the title and the pagination limit dropdown:

import { postsPaginationAtom } from "@/atoms/async-atom";
import { useAtom } from "jotai";
import React from "react";

type Props = {};

const limits = [5, 10, 15, 20];
export default function Header({}: Props) {
const [pagination, setPagination] = useAtom(postsPaginationAtom);

const onLimitChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setPagination({ ...pagination, limit: parseInt(e.target.value), start: 0 });
};

return (
<div className="flex flex-row items-center space-x-4 p-2 mb-4 w-full justify-between">
<h1>Posts</h1>
<select
value={pagination.limit}
onChange={onLimitChange}
className="font-bold py-2 px-4 rounded border border-gray-300"
>
{limits.map((limit) => (
<option key={limit} value={limit}>
{limit}
</option>
))}
</select>
</div>
);
}

This is how you can use async atoms in Jotai to fetch data asynchronously and manage the state of the data in your application.

Demo

Here is a demo of the async atom example:

Full Demo: https://jotai-example.rasit.me/

Github Repository: https://github.com/rasitcolakel/next-js-jotai

Conclusion

In this article, we learned about Jotai, a simple and flexible state management library for React. We learned how to create atoms, derived atoms, and async atoms using Jotai. We also learned how to use Jotai in a Next.js application. Jotai is a powerful state management library that can help you manage the state of your application in a simple and efficient way. I hope you found this article helpful. If you have any questions or feedback, feel free to reach out to me on Twitter. Thank you for reading!

References

--

--

No responses yet