본문으로 건너뛰기 Jotai Complete Guide

Jotai Complete Guide

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:

  1. Atoms are units of state
  2. Derived atoms for computed values
  3. Write-only atoms for actions
  4. Async atoms with Suspense
  5. 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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Jotai, React, State Management, JavaScript, TypeScript, Hooks.