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)是状态管理的基本单位
  • 使用 useAtomuseAtomValueuseSetAtom 与原子交互
  • 派生原子可以基于其他原子计算得出
  • 异步原子支持处理异步数据
  • 原子可以轻松组合,形成复杂的状态依赖
  • 内置工具支持状态持久化和性能优化

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

相关推荐
LLLLYYYRRRRRTT10 分钟前
MariaDB 数据库管理与web服务器
前端·数据库·mariadb
胡gh11 分钟前
什么是瀑布流?用大白话给你讲明白!
前端·javascript·面试
universe_0117 分钟前
day22|学习前端ts语言
前端·笔记
teeeeeeemo20 分钟前
一些js数组去重的实现算法
开发语言·前端·javascript·笔记·算法
Zz_waiting.22 分钟前
Javaweb - 14.1 - 前端工程化
前端·es6
掘金安东尼24 分钟前
前端周刊第426期(2025年8月4日–8月10日)
前端·javascript·面试
Abadbeginning24 分钟前
FastSoyAdmin导出excel报错‘latin-1‘ codec can‘t encode characters in position 41-54
前端·javascript·后端
ZXT26 分钟前
WebAssembly
前端
卢叁26 分钟前
Flutter开发环境安装指南
前端·flutter
curdcv_po44 分钟前
Three.js,闲谈3D——智慧XX
前端