当前位置: 首页 > ds >正文

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

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

什么是 Jotai?

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

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

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

安装 Jotai

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

# 使用 npm
npm install jotai# 使用 yarn
yarn add jotai# 使用 pnpm
pnpm add jotai

核心概念:原子(Atom)

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

创建第一个原子

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

import { atom } from 'jotai';// 创建一个初始值为 0 的计数器原子
const countAtom = atom(0);

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

在组件中使用原子

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

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 钩子只返回原子的值,不提供修改方法,适合只读场景:

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

useSetAtom:只写操作

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

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 方法,用于获取其他原子的值:

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

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

组合多个原子

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

// 创建两个原子
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><inputplaceholder="名"value={firstName}onChange={(e) => setFirstName(e.target.value)}/><inputplaceholder="姓"value={lastName}onChange={(e) => setLastName(e.target.value)}/><p>全名: {fullName}</p></div>);
}

异步原子(Async Atoms)

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

创建异步原子

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

// 异步获取用户数据的原子
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>);
}

带参数的异步原子

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

// 创建一个接收 ID 参数的原子
const postAtom = atom((get) => get, // 这是一个占位符,实际我们会在使用时传递参数async (get, set, id) => { // 第三个参数是我们传递的 IDconst 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 可以帮助我们实现状态的持久化。

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 的一大优势是可以轻松组合多个原子,形成复杂的状态依赖关系。

// 基础原子
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 来缓存计算结果:

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

选择性订阅

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

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>;
}

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

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

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}><inputtype="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}><inputtype="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 吧,体验原子化状态管理的便捷与高效!

http://www.xdnf.cn/news/17661.html

相关文章:

  • Code Exercising Day 10 of “Code Ideas Record“:StackQueue part02
  • SQL三剑客:DELETE、TRUNCATE、DROP全解析
  • CentOS7.9 离线安装mysql数据库
  • CPP继承
  • Windows执行kubectl提示拒绝访问【Windows安装k8s】
  • `sk_buff` 结构体详解(包含全生命周期解析)
  • 数学建模:控制预测类问题
  • 全面了解机器语言之kmeans
  • 010601抓包工具及证书安装-基础入门-网络安全
  • 【Matplotlib】中文显示问题
  • 企业级WEB应用服务器TOMCAT — WEB技术详细部署
  • 正点原子esp32s3探测土壤湿度
  • openpnp - 顶部相机如果超过6.5米影响通讯质量,可以加USB3.0信号放大器延长线
  • Effective C++ 条款34:区分接口继承和实现继承
  • 数据库面试题集
  • DFT的几点理解(二)
  • 计算二分类误差时的常见错误及解决方案
  • 农经权二轮延包—已有软件与后续研究
  • Spring之【详解AOP】
  • NLP 2025全景指南:从分词到128专家MoE模型,手撕BERT情感分析实战(第四章)
  • scanpy单细胞转录组python教程(三):单样本数据分析之数据标准化、特征选择、细胞周期计算、回归等
  • 制动电阻烧损记录学习
  • Spark执行计划与UI分析
  • JVM调优好用的内存分析工具!
  • jvm有哪些垃圾回收器,实际中如何选择?
  • 工业相机选择规则
  • leetcode经典题目——单调栈
  • 机器学习第八课之K-means聚类算法
  • Android 16 KB页面大小适配的权威技术方案总结
  • Android Camera 打开和拍照APK源码