Jotai:React轻量级状态管理新选择

Jotai 是一个受 Recoil 启发的 React 状态管理库,采用"原子化"的状态管理理念,让状态管理变得简单、灵活且可预测。本教程将从零开始,带你全面掌握 Jotai 的使用方法。

什么是 Jotai?

Jotai 发音为 "joe-tie",源自日语"状態"(じょうたい - jōtai),意为"状态"。它的核心思想是将状态分割成一个个独立的"原子"(atom),组件可以精确订阅所需的状态,避免不必要的重渲染。

与 Redux 等传统状态管理库相比,Jotai 具有以下优势:

  • 无需大量模板代码
  • 原子化设计,精确更新
  • 简洁的 API,易于学习
  • 优秀的 TypeScript 支持
  • 与 React 生态无缝集成

安装 Jotai

首先,我们需要安装 Jotai 包:

bash 复制代码
# 使用 npm
npm install jotai

# 使用 yarn
yarn add jotai

# 使用 pnpm
pnpm add jotai

核心概念:原子(Atom)

在 Jotai 中,"原子"(atom)是状态管理的基本单位。一个原子代表一个可共享的状态片段,可以是任何类型的值(数字、字符串、对象等)。

创建第一个原子

使用 atom 函数可以创建一个原子:

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

// 创建一个初始值为 0 的计数器原子
const countAtom = atom(0);

这是一个最基本的"可写原子"(writable atom),既可以读取其值,也可以修改它。

在组件中使用原子

要在组件中使用原子,我们需要用到 useAtom 钩子,它返回一个数组 [value, setValue],类似于 React 的 useState:

js 复制代码
import { useAtom } from 'jotai';

function Counter() {
  // 解构出值和更新函数
  const [count, setCount] = useAtom(countAtom);
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>加 1</button>
      <button onClick={() => setCount(c => c - 1)}>减 1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

setCount 可以直接接收新值,也可以接收一个函数,该函数接收当前值并返回新值。

只读与只写操作

在很多场景下,组件可能只需要读取状态或只需要修改状态。Jotai 提供了专门的钩子来处理这些情况。

useAtomValue:只读操作

useAtomValue 钩子只返回原子的值,不提供修改方法,适合只读场景:

js 复制代码
import { useAtomValue } from 'jotai';

function CountDisplay() {
  // 只获取值,不获取更新函数
  const count = useAtomValue(countAtom);
  
  return <div>当前计数: {count}</div>;
}

useSetAtom:只写操作

useSetAtom 钩子只返回修改原子值的函数,不获取当前值,适合只需要修改状态的场景:

js 复制代码
import { useSetAtom } from 'jotai';

function CountControls() {
  // 只获取更新函数
  const setCount = useSetAtom(countAtom);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>加 1</button>
      <button onClick={() => setCount(c => c - 1)}>减 1</button>
    </div>
  );
}

这种分离不仅让代码意图更清晰,还能避免不必要的重渲染。

派生原子(Derived Atoms)

派生原子是基于其他原子计算得出的原子,它的值会随着依赖原子的变化而自动更新。

创建派生原子

通过向 atom 函数传递一个函数,可以创建派生原子。这个函数接收一个 get 方法,用于获取其他原子的值:

js 复制代码
// 派生原子:计算计数的两倍
const doubleCountAtom = atom((get) => {
  const count = get(countAtom);
  return count * 2;
});

// 使用派生原子
function DoubleCountDisplay() {
  const doubleCount = useAtomValue(doubleCountAtom);
  return <div>计数的两倍: {doubleCount}</div>;
}

当 countAtom 的值发生变化时,doubleCountAtom 的值会自动重新计算,所有使用 doubleCountAtom 的组件都会更新。

组合多个原子

派生原子可以依赖多个原子,形成复杂的状态计算:

js 复制代码
// 创建两个原子
const firstNameAtom = atom('');
const lastNameAtom = atom('');

// 派生原子:组合姓名
const fullNameAtom = atom((get) => {
  const firstName = get(firstNameAtom);
  const lastName = get(lastNameAtom);
  return `${firstName} ${lastName}`;
});

// 使用这些原子
function NameForm() {
  const [firstName, setFirstName] = useAtom(firstNameAtom);
  const [lastName, setLastName] = useAtom(lastNameAtom);
  const fullName = useAtomValue(fullNameAtom);
  
  return (
    <div>
      <input
        placeholder="名"
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
      />
      <input
        placeholder="姓"
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
      />
      <p>全名: {fullName}</p>
    </div>
  );
}

异步原子(Async Atoms)

Jotai 对异步操作有很好的支持,可以轻松创建依赖异步数据的原子。

创建异步原子

异步原子的创建方式与普通派生原子类似,但返回一个 Promise:

js 复制代码
// 异步获取用户数据的原子
const userAtom = atom(async () => {
  const response = await fetch('https://api.example.com/user');
  const user = await response.json();
  return user;
});

// 使用异步原子
function UserProfile() {
  const user = useAtomValue(userAtom);
  
  // 异步原子的初始状态是 Promise,所以需要处理加载和错误状态
  if (user === undefined) {
    return <div>加载中...</div>;
  }
  
  if (user instanceof Error) {
    return <div>出错了: {user.message}</div>;
  }
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

带参数的异步原子

我们可以创建接收参数的异步原子,实现更灵活的数据获取:

js 复制代码
// 创建一个接收 ID 参数的原子
const postAtom = atom(
  (get) => get, // 这是一个占位符,实际我们会在使用时传递参数
  async (get, set, id) => { // 第三个参数是我们传递的 ID
    const response = await fetch(`https://api.example.com/posts/${id}`);
    return response.json();
  }
);

// 使用带参数的原子
function Post({ postId }) {
  // 使用一个派生原子来传递参数
  const specificPostAtom = atom((get) => get(postAtom, postId));
  const post = useAtomValue(specificPostAtom);
  
  if (!post) return <div>加载中...</div>;
  
  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
    </article>
  );
}

原子的持久化

Jotai 提供了 jotai/utils 模块,包含一些实用工具,其中 persistAtom 可以帮助我们实现状态的持久化。

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

// 创建一个会自动持久化到 localStorage 的原子
const themeAtom = atomWithStorage('theme', 'light'); // 第一个参数是存储键名,第二个是默认值

function ThemeToggle() {
  const [theme, setTheme] = useAtom(themeAtom);
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      当前主题: {theme},点击切换
    </button>
  );
}

atomWithStorage 会自动将状态保存到 localStorage,并在应用重新加载时恢复。

原子的组合与依赖管理

Jotai 的一大优势是可以轻松组合多个原子,形成复杂的状态依赖关系。

js 复制代码
// 基础原子
const todosAtom = atom([]);
const filterAtom = atom('all'); // 'all', 'active', 'completed'

// 派生原子:根据筛选条件过滤 todos
const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filter = get(filterAtom);
  
  switch (filter) {
    case 'active':
      return todos.filter(todo => !todo.completed);
    case 'completed':
      return todos.filter(todo => todo.completed);
    default:
      return todos;
  }
});

// 派生原子:计算未完成的 todo 数量
const remainingTodosCountAtom = atom((get) => {
  const todos = get(todosAtom);
  return todos.filter(todo => !todo.completed).length;
});

这种设计让每个状态片段保持独立,同时又能灵活组合,大大提高了代码的可维护性。

性能优化

Jotai 的原子化设计本身就有助于性能优化,因为组件只会在其订阅的原子发生变化时重新渲染。不过,我们还可以进一步优化。

避免不必要的计算

对于计算成本较高的派生原子,可以使用 atomWithCache 来缓存计算结果:

js 复制代码
import { atomWithCache } from 'jotai/utils';

// 计算成本高的派生原子
const expensiveCalculationAtom = atomWithCache((get) => {
  const data = get(largeDataSetAtom);
  // 执行复杂计算...
  return result;
});

选择性订阅

当原子存储对象时,可以使用 selectAtom 只订阅对象的特定属性:

js 复制代码
import { selectAtom } from 'jotai/utils';

// 用户信息原子
const userAtom = atom({
  name: '张三',
  age: 30,
  address: '北京市'
});

// 只订阅用户的姓名
const userNameAtom = selectAtom(userAtom, (user) => user.name);

// 这个组件只会在用户姓名变化时重新渲染
function UserNameDisplay() {
  const name = useAtomValue(userNameAtom);
  return <div>姓名: {name}</div>;
}

实际应用示例:待办事项应用

让我们综合运用所学知识,创建一个完整的待办事项应用:

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

// 1. 定义原子
// 持久化存储的待办事项原子
const todosAtom = atomWithStorage('todos', []);

// 筛选条件原子
const filterAtom = atomWithStorage('todoFilter', 'all');

// 派生原子:过滤后的待办事项
const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filter = get(filterAtom);
  
  switch (filter) {
    case 'active':
      return todos.filter(todo => !todo.completed);
    case 'completed':
      return todos.filter(todo => todo.completed);
    default:
      return todos;
  }
});

// 派生原子:待办事项统计
const todoStatsAtom = atom((get) => {
  const todos = get(todosAtom);
  const completed = todos.filter(todo => todo.completed).length;
  const total = todos.length;
  return {
    total,
    completed,
    remaining: total - completed
  };
});

// 2. 组件
function TodoInput() {
  const [text, setText] = useState('');
  const [todos, setTodos] = useAtom(todosAtom);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    
    // 添加新的待办事项
    setTodos(prev => [
      ...prev,
      {
        id: Date.now(),
        text,
        completed: false
      }
    ]);
    
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="添加新的待办事项..."
      />
      <button type="submit">添加</button>
    </form>
  );
}

function TodoList() {
  const todos = useAtomValue(filteredTodosAtom);
  const [, setTodos] = useAtom(todosAtom);
  
  const toggleTodo = (id) => {
    setTodos(prev => 
      prev.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };
  
  const deleteTodo = (id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };
  
  if (todos.length === 0) {
    return <p>没有待办事项</p>;
  }
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

function TodoFilters() {
  const [filter, setFilter] = useAtom(filterAtom);
  
  return (
    <div>
      <button 
        onClick={() => setFilter('all')}
        style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
      >
        全部
      </button>
      <button 
        onClick={() => setFilter('active')}
        style={{ fontWeight: filter === 'active' ? 'bold' : 'normal' }}
      >
        未完成
      </button>
      <button 
        onClick={() => setFilter('completed')}
        style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal' }}
      >
        已完成
      </button>
    </div>
  );
}

function TodoStats() {
  const { total, completed, remaining } = useAtomValue(todoStatsAtom);
  const [, setTodos] = useAtom(todosAtom);
  
  const clearCompleted = () => {
    setTodos(prev => prev.filter(todo => !todo.completed));
  };
  
  return (
    <div>
      <p>
        共 {total} 项,已完成 {completed} 项,剩余 {remaining} 项
      </p>
      {completed > 0 && (
        <button onClick={clearCompleted}>清除已完成</button>
      )}
    </div>
  );
}

// 3. 组合应用
function TodoApp() {
  return (
    <div>
      <h1>待办事项</h1>
      <TodoInput />
      <TodoFilters />
      <TodoList />
      <TodoStats />
    </div>
  );
}

总结

Jotai 以其简洁的 API 和原子化的设计理念,为 React 应用提供了一种直观而高效的状态管理方案。通过本教程,你应该已经掌握了 Jotai 的核心概念和使用方法:

  • 原子(atom)是状态管理的基本单位
  • 使用 useAtom、useAtomValue 和 useSetAtom 与原子交互
  • 派生原子可以基于其他原子计算得出
  • 异步原子支持处理异步数据
  • 原子可以轻松组合,形成复杂的状态依赖
  • 内置工具支持状态持久化和性能优化

Jotai 的学习曲线平缓,但功能强大,适合各种规模的 React 应用。开始在你的项目中尝试使用 Jotai 吧,体验原子化状态管理的便捷与高效!

相关推荐
brien927 分钟前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance42 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M1 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写1 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 小时前
前沿视界
前端
kyriewen1 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易2 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒2 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端