Jotai is a modern state management library for React that takes inspiration from Recoil’s atomic model but with a simpler API and smaller bundle size. It’s particularly well-suited for applications that need flexible, composable state management without the boilerplate of Redux.
Core Concepts
1. Atoms: The Building Blocks
Atoms are units of state that can be read and written from any component.
import { atom } from 'jotai';
// Create an atom
const countAtom = atom(0);
// Derived atom (computed state)
const doubledCountAtom = atom((get) => get(countAtom) * 2);
Basic Usage
2. Reading and Writing Atoms
import { useAtom } from 'jotai';
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
3. Derived State
function Display() {
const [doubled] = useAtom(doubledCountAtom);
return <div>Doubled: {doubled}</div>;
}
Advanced Patterns
4. Async Atoms (Suspense-ready)
const userAtom = atom(async () => {
const response = await fetch('/api/user');
return response.json();
});
function UserProfile() {
const [user] = useAtom(userAtom);
return <div>{user.name}</div>;
}
// Wrap in Suspense boundary
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
5. Write-only Atoms (Actions)
const incrementCountAtom = atom(
null, // read not needed
(get, set) => set(countAtom, get(countAtom) + 1)
);
function IncrementButton() {
const [, increment] = useAtom(incrementCountAtom);
return <button onClick={increment}>Increment</button>;
}
6. Atom Families (Parameterized Atoms)
import { atomFamily } from 'jotai/utils';
const todoAtomFamily = atomFamily((id) => atom({}));
function Todo({ id }) {
const [todo, setTodo] = useAtom(todoAtomFamily(id));
// ...
}
Performance Optimization
7. Selectors for Optimized Renders
const userFirstNameAtom = atom(
(get) => get(userAtom).firstName
);
function FirstNameDisplay() {
const [firstName] = useAtom(userFirstNameAtom);
// Only re-renders when firstName changes
return <div>{firstName}</div>;
}
8. Memoization with useMemoAtom
import { useMemoAtom } from 'jotai/utils';
function ExpensiveComponent() {
const memoizedAtom = useMemoAtom(() => atom(expensiveComputation()));
const [value] = useAtom(memoizedAtom);
// ...
}
Real-World Examples
9. Shopping Cart Implementation
const cartAtom = atom<CartItem[]>([]);
const cartTotalAtom = atom((get) =>
get(cartAtom).reduce((sum, item) => sum + item.price * item.quantity, 0)
);
const addToCartAtom = atom(null, (get, set, item: CartItem) => {
const cart = get(cartAtom);
const existing = cart.find(i => i.id === item.id);
if (existing) {
set(cartAtom, cart.map(i =>
i.id === item.id ? {...i, quantity: i.quantity + 1} : i
));
} else {
set(cartAtom, [...cart, {...item, quantity: 1}]);
}
});
// Usage in components
function AddToCartButton({ product }) {
const [, addToCart] = useAtom(addToCartAtom);
return <button onClick={() => addToCart(product)}>Add to Cart</button>;
}
10. Theme Switching
const themeAtom = atom<'light' | 'dark'>('light');
const themeStylesAtom = atom((get) => {
const theme = get(themeAtom);
return {
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
};
});
function ThemeSwitcher() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}
function ThemedComponent() {
const [styles] = useAtom(themeStylesAtom);
return <div style={styles}>Themeable content</div>;
}
Integration Patterns
11. With Next.js (SSR Support)
// lib/store.js
import { createStore } from 'jotai';
import { useHydrateAtoms } from 'jotai/utils';
export const store = createStore();
// For SSR hydration
export function HydrateAtoms({ initialValues, children }) {
useHydrateAtoms(initialValues);
return children;
}
// pages/_app.js
function MyApp({ Component, pageProps }) {
return (
<HydrateAtoms initialValues={pageProps.initialValues || []}>
<Component {...pageProps} />
</HydrateAtoms>
);
}
12. With React Query
const fetchUserAtom = atom(async (get) => {
const userId = get(userIdAtom);
const { data } = await queryClient.getQueryData(['user', userId]) ||
await queryClient.fetchQuery(['user', userId], () => fetchUser(userId));
return data;
});
function UserProfile() {
const [user] = useAtom(fetchUserAtom);
// ...
}
Best Practices
- Colocate Atoms: Keep atoms close to where they’re used
- Small Atoms: Prefer many small atoms over few large ones
- Derived State: Use computed atoms for complex transformations
- Type Safety: Use TypeScript for better developer experience
- Suspense Boundaries: Wrap async atoms in Suspense
- Atom Families: Use for dynamic or parameterized state
- Performance: Memoize expensive computations
Comparison with Other State Solutions
Feature | Jotai | Redux | Recoil | Zustand |
---|---|---|---|---|
Bundle Size | ~3KB | ~7KB | ~30KB | ~1KB |
Boilerplate | Minimal | High | Moderate | Minimal |
Learning Curve | Easy | Steep | Moderate | Easy |
Async Support | Built-in | Middleware | Built-in | Manual |
DevTools | Basic | Excellent | Basic | Basic |
SSR Support | Good | Good | Limited | Good |
Jotai shines when you need:
- Simple yet powerful state management
- Composability and code reuse
- Good performance out of the box
- Minimal boilerplate
- Flexible patterns for derived state
By leveraging Jotai’s atomic model, you can create highly maintainable React applications with state that’s both easy to manage and optimized for performance. The library’s focus on primitive building blocks (atoms) makes it particularly suitable for medium to large applications where state needs to be shared across many components without prop drilling.