Jotai Complete Guide
이 글의 핵심
Jotai is a primitive and flexible state management library for React. It takes an atomic approach to global state with a minimal API inspired by Recoil.
Introduction
Jotai is a primitive and flexible state management solution for React. It takes an atomic approach to global React state, inspired by Recoil with a more minimal API.
The Problem
Context API:
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Alice', age: 30 });
return (
<UserContext.Provider value={user}>
<ComponentA /> {/* Re-renders on ANY user change */}
<ComponentB /> {/* Re-renders on ANY user change */}
</UserContext.Provider>
);
}
Jotai:
const nameAtom = atom('Alice');
const ageAtom = atom(30);
function ComponentA() {
const [name] = useAtom(nameAtom);
return <div>{name}</div>; // Only re-renders when name changes
}
function ComponentB() {
const [age] = useAtom(ageAtom);
return <div>{age}</div>; // Only re-renders when age changes
}
1. Installation
npm install jotai
2. Atoms (Primitive State)
Basic Atom
import { atom, useAtom } from 'jotai';
// Define atom
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(c => c + 1)}>Increment (updater)</button>
</div>
);
}
Read-only Hook
import { useAtomValue } from 'jotai';
function Display() {
const count = useAtomValue(countAtom); // Read-only
return <div>{count}</div>;
}
Write-only Hook
import { useSetAtom } from 'jotai';
function Controls() {
const setCount = useSetAtom(countAtom); // Write-only
return (
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
);
}
3. Derived Atoms
import { atom } from 'jotai';
const priceAtom = atom(100);
const quantityAtom = atom(2);
// Read-only derived atom
const totalAtom = atom((get) => {
const price = get(priceAtom);
const quantity = get(quantityAtom);
return price * quantity;
});
function Cart() {
const [price, setPrice] = useAtom(priceAtom);
const [quantity, setQuantity] = useAtom(quantityAtom);
const total = useAtomValue(totalAtom);
return (
<div>
<input value={price} onChange={e => setPrice(+e.target.value)} />
<input value={quantity} onChange={e => setQuantity(+e.target.value)} />
<p>Total: ${total}</p>
</div>
);
}
Writable Derived Atoms
const celsiusAtom = atom(0);
const fahrenheitAtom = atom(
(get) => get(celsiusAtom) * 9/5 + 32, // Read
(get, set, newValue) => { // Write
set(celsiusAtom, (newValue - 32) * 5/9);
}
);
function Temperature() {
const [celsius, setCelsius] = useAtom(celsiusAtom);
const [fahrenheit, setFahrenheit] = useAtom(fahrenheitAtom);
return (
<div>
<input value={celsius} onChange={e => setCelsius(+e.target.value)} />°C
<input value={fahrenheit} onChange={e => setFahrenheit(+e.target.value)} />°F
</div>
);
}
4. Async Atoms
const userIdAtom = atom(1);
const userAtom = atom(async (get) => {
const id = get(userIdAtom);
const res = await fetch(`/api/users/${id}`);
return res.json();
});
function UserProfile() {
const [user] = useAtom(userAtom);
return <div>{user.name}</div>;
}
With Suspense
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
);
}
5. Actions (Write-only Atoms)
const todosAtom = atom([]);
const addTodoAtom = atom(
null, // No read
(get, set, text: string) => {
const todos = get(todosAtom);
set(todosAtom, [...todos, { id: Date.now(), text, done: false }]);
}
);
function TodoForm() {
const addTodo = useSetAtom(addTodoAtom);
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
addTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}
6. Atom Families
import { atomFamily } from 'jotai/utils';
const userAtomFamily = atomFamily((id: number) =>
atom(async () => {
const res = await fetch(`/api/users/${id}`);
return res.json();
})
);
function UserProfile({ userId }: { userId: number }) {
const [user] = useAtom(userAtomFamily(userId));
return <div>{user.name}</div>;
}
7. Utils
atomWithStorage
import { atomWithStorage } from 'jotai/utils';
const darkModeAtom = atomWithStorage('darkMode', false);
function ThemeToggle() {
const [darkMode, setDarkMode] = useAtom(darkModeAtom);
return (
<button onClick={() => setDarkMode(!darkMode)}>
{darkMode ? 'Light' : 'Dark'} Mode
</button>
);
}
atomWithReducer
import { atomWithReducer } from 'jotai/utils';
const countReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
default: return state;
}
};
const countAtom = atomWithReducer(0, countReducer);
function Counter() {
const [count, dispatch] = useAtom(countAtom);
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
atomWithReset
import { atomWithReset, useResetAtom } from 'jotai/utils';
const filterAtom = atomWithReset('');
function SearchFilter() {
const [filter, setFilter] = useAtom(filterAtom);
const resetFilter = useResetAtom(filterAtom);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<button onClick={resetFilter}>Clear</button>
</div>
);
}
8. Real-World Example: Todo App
import { atom, useAtom, useSetAtom, useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
interface Todo {
id: number;
text: string;
done: boolean;
}
const todosAtom = atomWithStorage<Todo[]>('todos', []);
const filterAtom = atom<'all' | 'active' | 'done'>('all');
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
if (filter === 'all') return todos;
return todos.filter(t => filter === 'done' ? t.done : !t.done);
});
const addTodoAtom = atom(
null,
(get, set, text: string) => {
const todos = get(todosAtom);
set(todosAtom, [...todos, { id: Date.now(), text, done: false }]);
}
);
const toggleTodoAtom = atom(
null,
(get, set, id: number) => {
const todos = get(todosAtom);
set(todosAtom, todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
}
);
function TodoApp() {
const [text, setText] = useState('');
const addTodo = useSetAtom(addTodoAtom);
const todos = useAtomValue(filteredTodosAtom);
const toggleTodo = useSetAtom(toggleTodoAtom);
const [filter, setFilter] = useAtom(filterAtom);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!text.trim()) return;
addTodo(text);
setText('');
};
return (
<div>
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add</button>
</form>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('done')}>Done</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.done ? '?? : '??} {todo.text}
</li>
))}
</ul>
</div>
);
}
9. DevTools
npm install --save-dev jotai-devtools
import { DevTools } from 'jotai-devtools';
function App() {
return (
<>
<DevTools />
<YourApp />
</>
);
}
10. Best Practices
1. Use Atom Families for Dynamic Data
// Good: scales to any number of users
const userFamily = atomFamily((id) => atom(/* ... */));
// Bad: hardcoded atoms
const user1Atom = atom(/* ... */);
const user2Atom = atom(/* ... */);
2. Separate Read and Write
// Good: optimized re-renders
const count = useAtomValue(countAtom);
const setCount = useSetAtom(countAtom);
// Okay: when you need both
const [count, setCount] = useAtom(countAtom);
3. Use Write-only Atoms for Actions
const addTodoAtom = atom(null, (get, set, text) => {
// Action logic
});
Summary
Jotai provides primitive, flexible React state:
- Atomic state management
- Minimal API - easy to learn
- TypeScript first
- Async support built-in
- No Provider needed
Key Takeaways:
- Atoms are units of state
- Derived atoms for computed values
- Write-only atoms for actions
- Async atoms with Suspense
- Fine-grained re-renders
Next Steps:
Resources:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Jotai is a primitive and flexible state management library for React. It takes an atomic approach to global state with a minimal API inspired by Recoil.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- MobX Complete Guide | Simple Reactive State Management
- Zustand Complete Guide: Simple State Management for React with TypeScript
- Jest Complete Guide | JavaScript Testing· Mocking
Keywords Covered in This Article (Related Search Terms)
This article covers Jotai, React, State Management, JavaScript, TypeScript, Hooks.