Jotai:React轻量级原子化状态管理,告别重渲染困扰

简介

Jotai 是一个为 React 提供的原子化状态管理库,采用自下而上的方法来进行状态管理。Jotai 受 Recoil 启发,通过组合原子来构建状态,并且渲染基于原子依赖性进行优化。这解决了 React 上下文的额外重新渲染问题,并消除了对 memoization 技术的需要。

核心特性

  • 原子化状态 -- 状态被分解为原子单元,可以独立管理和组合
  • 零配置 -- 无需像 Redux 那样的复杂配置和样板代码
  • 类型安全 -- 完全支持 TypeScript,提供良好的类型推断
  • 高性能 -- 自动优化渲染,避免不必要的组件重渲染
  • 轻量级 -- 核心包仅 2.4kB,API 简洁易用
  • 灵活性 -- 支持同步和异步状态,易于派生和组合

快速开始

安装

复制代码
npm install jotai
# 或
yarn add jotai
# 或
pnpm add jotai

基础用法

复制代码
import { atom, useAtom } from 'jotai'

// 创建一个原子状态
const countAtom = atom(0)

function Counter() {
  // 使用原子状态,类似于 useState
  const [count, setCount] = useAtom(countAtom)

  return (
    <div className="flex flex-col items-center justify-center min-h-[300px] p-6">
      <h1 className="text-3xl font-medium mb-6 text-gray-800">
        Count: <span className="text-4xl font-bold">{count}</span>
      </h1>
      <div className="flex gap-3">
        <button 
          onClick={() => setCount(count + 1)}
          className="px-4 py-2 bg-blue-500 text-white font-medium rounded hover:bg-blue-600"
        >
          Increment
        </button>
        <button 
          onClick={() => setCount(count - 1)}
          className="px-4 py-2 bg-red-500 text-white font-medium rounded hover:bg-red-600"
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

export default Counter

原子类型

基础原子

基础原子是最简单的状态单元,可以存储任何类型的值。

复制代码
import { atom } from 'jotai'

// 基础类型
const boolAtom = atom(true)
const numberAtom = atom(42)
const stringAtom = atom('hello')

// 复杂类型
const objectAtom = atom({ name: 'John', age: 30 })
const arrayAtom = atom(['apple', 'banana', 'orange'])

派生原子

派生原子可以基于其他原子计算出新的状态,类似于 Vue 的计算属性或 MobX 的计算值。

复制代码
import { atom } from 'jotai'

const countAtom = atom(0)

// 只读派生原子
const doubleCountAtom = atom((get) => get(countAtom) * 2)

// 可读写派生原子
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])

const derivedAtom = atom(
  (get) => ({
    country: get(countryAtom),
    cities: get(citiesAtom),
  }),
  (get, set, newValue) => {
    // 可以同时更新多个原子
    set(countryAtom, newValue.country)
    set(citiesAtom, newValue.cities)
  }
)

使用原子

Jotai 提供了几种使用原子的方式,根据不同的使用场景选择合适的 Hook。

useAtom

最基本的 Hook,类似于 React 的 useState,用于读取和更新原子状态。

复制代码
import { atom, useAtom } from 'jotai'

const textAtom = atom('hello')

function TextInput() {
  const [text, setText] = useAtom(textAtom)

  return (
    <input 
      value={text} 
      onChange={(e) => setText(e.target.value)} 
    />
  )
}

useAtomValue 和 useSetAtom

当组件只需要读取或只需要写入原子状态时,可以使用这两个 Hook 来优化性能。

复制代码
import { atom, useAtomValue, useSetAtom } from 'jotai'

const countAtom = atom(0)

// 只读组件
function DisplayCount() {
  const count = useAtomValue(countAtom)
  return <div>Count: {count}</div>
}

// 只写组件
function Controls() {
  const setCount = useSetAtom(countAtom)

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => setCount(c => c - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

高级用法

异步原子

Jotai 支持异步原子,可以处理异步数据获取和更新。

复制代码
import { atom, useAtom } from 'jotai'

// 异步读取原子
const userAtom = atom(async () => {
  const response = await fetch('https://api.example.com/user')
  return response.json()
})

// 异步写入原子
const postAtom = atom(
  null,
  async (get, set, newPost) => {
    const response = await fetch('https://api.example.com/posts', {
      method: 'POST',
      body: JSON.stringify(newPost),
    })
    const result = await response.json()
    // 可以更新其他原子
    set(postsAtom, [...get(postsAtom), result])
    return result
  }
)

function AsyncComponent() {
  const [user, setUser] = useAtom(userAtom)
  const [, createPost] = useAtom(postAtom)

  // 使用 React Suspense 处理加载状态
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <button onClick={() => createPost({ title: 'New Post' })}>Create Post</button>
    </div>
  )
}

持久化

Jotai 提供了 atomWithStorage 工具函数,可以轻松实现状态的持久化。

复制代码
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

// 自动保存到 localStorage
const darkModeAtom = atomWithStorage('darkMode', false)

function ThemeToggle() {
  const [darkMode, setDarkMode] = useAtom(darkModeAtom)

  return (
    <div>
      <h1>Current theme: {darkMode ? 'Dark' : 'Light'}</h1>
      <button onClick={() => setDarkMode(!darkMode)}>
        Toggle theme
      </button>
    </div>
  )
}

原子族 (atomFamily)

原子族用于创建一组相关的原子,每个原子都有自己的状态,但共享相同的行为。

复制代码
import { useAtom } from 'jotai'
import { atomFamily } from 'jotai/utils'

// 创建一个原子族,每个 ID 对应一个原子
const todoAtomFamily = atomFamily(
  (id) => atom({ id, text: '', completed: false }),
  (a, b) => a === b
)

function TodoItem({ id }) {
  const [todo, setTodo] = useAtom(todoAtomFamily(id))

  return (
    <div>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => setTodo({ ...todo, completed: !todo.completed })}
      />
      <input
        value={todo.text}
        onChange={(e) => setTodo({ ...todo, text: e.target.value })}
      />
    </div>
  )
}

实际应用示例

复制代码
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

// 创建原子状态
const todosAtom = atomWithStorage('todos', []); // 存储所有待办事项,使用 atomWithStorage 自动持久化到 localStorage
const todoInputAtom = atom(''); // 存储输入框的值

// 过滤类型:全部、已完成、未完成
const filterTypeAtom = atomWithStorage('filterType', 'all');

// 派生状态 - 计算已完成和未完成的任务数量
const todoStatsAtom = atom((get) => {
  const todos = get(todosAtom);
  const total = todos.length;
  const completed = todos.filter(todo => todo.completed).length;
  const uncompleted = total - completed;
  return { total, completed, uncompleted };
});

// 派生状态 - 根据过滤类型筛选任务
const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filterType = get(filterTypeAtom);

  switch (filterType) {
    case 'completed':
      return todos.filter(todo => todo.completed);
    case 'active':
      return todos.filter(todo => !todo.completed);
    default:
      return todos;
  }
});

export default function CssDemo() {
  const [todos, setTodos] = useAtom(todosAtom);
  const [filteredTodos] = useAtom(filteredTodosAtom);
  const [todoInput, setTodoInput] = useAtom(todoInputAtom);
  const [todoStats] = useAtom(todoStatsAtom);
  const [filterType, setFilterType] = useAtom(filterTypeAtom);

  // 添加新的待办事项
  const addTodo = () => {
    if (todoInput.trim() === '') return;

    const newTodo = {
      id: Date.now(),
      text: todoInput,
      completed: false
    };

    setTodos([...todos, newTodo]);
    setTodoInput('');
  };

  // 切换待办事项的完成状态
  const toggleTodo = (id) => {
    setTodos(
      todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // 删除待办事项
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // 清除所有已完成的任务
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

  // 全部标记为已完成/未完成
  const markAllAs = (completed) => {
    setTodos(todos.map(todo => ({ ...todo, completed })));
  };

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-sm">
      <h1 className="text-2xl font-bold text-gray-800 mb-4 text-center">Todo List</h1>

      {/* 任务统计信息 */}
      <div className="flex justify-between text-sm text-gray-500 mb-5 bg-gray-50 p-2 rounded">
        <span className="px-2 py-1 bg-white rounded shadow-sm">总计: {todoStats.total}</span>
        <span className="px-2 py-1 bg-white rounded shadow-sm">已完成: {todoStats.completed}</span>
        <span className="px-2 py-1 bg-white rounded shadow-sm">未完成: {todoStats.uncompleted}</span>
      </div>

      {/* 添加待办事项表单 */}
      <div className="flex mb-5">
        <input
          type="text"
          value={todoInput}
          onChange={(e) => setTodoInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="添加新的待办事项..."
          className="flex-1 px-4 py-2 border border-gray-300 rounded-l focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
        />
        <button
          onClick={addTodo}
          className="px-4 py-2 bg-blue-500 text-white font-medium rounded-r hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-colors"
        >
          添加
        </button>
      </div>

      {/* 过滤选项 */}
      <div className="flex justify-center space-x-2 mb-4">
        <button
          onClick={() => setFilterType('all')}
          className={`px-4 py-1.5 text-sm rounded-full transition-colors ${filterType === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
        >
          全部
        </button>
        <button
          onClick={() => setFilterType('active')}
          className={`px-4 py-1.5 text-sm rounded-full transition-colors ${filterType === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
        >
          未完成
        </button>
        <button
          onClick={() => setFilterType('completed')}
          className={`px-4 py-1.5 text-sm rounded-full transition-colors ${filterType === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
        >
          已完成
        </button>
      </div>

      {/* 待办事项列表 */}
      <ul className="space-y-2 mb-4 max-h-60 overflow-y-auto pr-1">
        {filteredTodos.length === 0 ? (
          <li className="text-gray-500 text-center py-6 border border-dashed border-gray-200 rounded-lg bg-gray-50">
            {todos.length === 0 ? '暂无待办事项' : '没有符合条件的待办事项'}
          </li>
        ) : (
          filteredTodos.map(todo => (
            <li 
              key={todo.id} 
              className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
            >
              <div className="flex items-center flex-1 min-w-0">
                <input
                  type="checkbox"
                  checked={todo.completed}
                  onChange={() => toggleTodo(todo.id)}
                  className="h-5 w-5 text-blue-500 rounded focus:ring-blue-500"
                />
                <span 
                  className={`ml-3 truncate ${todo.completed ? 'line-through text-gray-400' : 'text-gray-800'}`}
                >
                  {todo.text}
                </span>
              </div>
              <button
                onClick={() => deleteTodo(todo.id)}
                className="text-red-500 hover:text-red-700 focus:outline-none ml-2 flex-shrink-0"
              >
                <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                  <path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
                </svg>
              </button>
            </li>
          ))
        )}
      </ul>

      {/* 底部操作栏 */}
      {todos.length > 0 && (
        <div className="flex justify-between pt-4 border-t border-gray-200">
          <button
            onClick={() => markAllAs(true)}
            className="text-sm text-blue-500 hover:text-blue-700 focus:outline-none transition-colors"
          >
            全部完成
          </button>
          <button
            onClick={() => markAllAs(false)}
            className="text-sm text-blue-500 hover:text-blue-700 focus:outline-none transition-colors"
          >
            全部取消
          </button>
          <button
            onClick={clearCompleted}
            className={`text-sm ${todoStats.completed === 0 ? 'text-gray-400 cursor-not-allowed' : 'text-red-500 hover:text-red-700'} focus:outline-none transition-colors`}
            disabled={todoStats.completed === 0}
          >
            清除已完成
          </button>
        </div>
      )}
    </div>
  );
}

与其他状态管理库的比较

Jotai vs Redux

  • 复杂度: Jotai 更简单,没有 actions、reducers、middleware 等概念
  • 样板代码: Jotai 几乎没有样板代码,而 Redux 需要大量样板代码
  • 学习曲线: Jotai 的学习曲线更平缓,API 更接近 React 原生 hooks
  • 适用场景: Jotai 适合中小型应用,Redux 适合大型、复杂的应用

Jotai vs Recoil

  • API: Jotai 的 API 更简洁,不需要 key 字符串
  • 大小: Jotai 更小巧 (2.4kB vs Recoil 的 ~20kB)
  • 配置: Jotai 不需要 Provider 包裹(虽然 SSR 时推荐使用)
  • TypeScript: Jotai 对 TypeScript 的支持更好

Jotai vs Zustand

  • 模型: Jotai 是原子模型,Zustand 是单一 store 模型
  • 集成: Jotai 与 React 集成更紧密,Zustand 可以在 React 外使用
  • 选择: 如果喜欢原子化状态,选 Jotai;如果喜欢单一 store,选 Zustand

最佳实践

  1. 原子粒度: 保持原子粒度适中,既不要过大也不要过小
  2. 原子组织: 将相关原子放在同一个文件中,便于管理
  3. 派生优先: 尽量使用派生原子而不是手动同步状态
  4. Hook 选择: 根据需要选择合适的 Hook (useAtom/useAtomValue/useSetAtom)
  5. 异步处理: 对于异步操作,使用 React Suspense 和 ErrorBoundary

总结

Jotai 是一个轻量级、高性能的 React 状态管理库,采用原子化的方式管理状态。它简化了全局状态管理,提供了优秀的开发体验和运行时性能。特别适合:

  • 中小型 React 应用
  • 需要简单状态管理的项目
  • 对性能有要求的应用
  • 喜欢函数式和原子化思想的开发者

通过原子化的状态管理方式,Jotai 既保持了使用的简单性,又提供了强大的状态组合能力,是 React 应用状态管理的绝佳选择。

Jotai:React轻量级原子化状态管理,告别重渲染困扰 - 高质量源码分享平台-免费下载各类网站源码与模板及前沿技术分享