Async Atoms — Easy State Management in Next.js with Jotai
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:
- We create a
postsPaginationAtom
atom to store the pagination information. - We create a
postsAtom
atom that fetches the posts based on the pagination information. - We create a
getPostsAtom
atom using theloadable
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!