React18状态管理方案之Jotai

721 阅读6分钟

Jotai 是什么

Jotai 是一个用于 React 的状态管理库,旨在提供简单、高效的状态管理解决方案。它采用原子(atom)的概念,每个原子代表一个独立的状态单元,允许在组件中灵活管理和订阅状态变化。Jotai 的 API 设计简洁,适合需要轻量级状态管理的应用程序,特别是在与其他状态管理库(如 Redux 或 MobX)配合使用时。jotai文档

Jotai 的优点包括:

  1. 简单易用:提供了直观的 API,容易上手。
  2. 灵活性:原子状态允许在不同组件之间共享状态,同时避免不必要的重渲染。
  3. 性能优化:只有依赖的组件在状态变化时才会重新渲染,提高性能。

Jotai的特性

安装

pnpm add jotai

Why Jotai

Jotai 的诞生是为了解决 React 中额外的重新渲染问题。要使用 React 上下文 (useContext + useState) 解决这个问题,需要很多上下文并面临一些问题。

  • Provider 地狱:您的根组件可能有许多上下文 Provider,这在技术上是可以的,有时需要在不同的子树中提供上下文。
  • 动态添加/删除:在运行时添加一个新的上下文不是很好,因为你需要添加一个新的 Provider 并且它的孩子将被重新挂载。

传统上,自上而下的解决方案是使用选择器。use-context-selector 库就是一个例子。 这种方法的问题是选择器函数需要返回引用相等的值以防止重新渲染,这通常需要 memoization 技术。

Jotai 受 Recoil 的启发,采用自下而上的原子模型方法。可以构建状态组合原子,并根据原子依赖性优化渲染。这避免了记忆的需要。

Jotai 有两个原则。

  • Primitive:它的基本接口很简单,比如 useState
  • 灵活:派生原子可以组合其他原子并启用带有副作用的 useReducer 样式。

task demo演示 使用 react18 + antd5 + ts + jotai2.x实现任务列表

image.png

实现一个简易task列表 包含新增删除功能。

// App.tsx
import React from 'react';
import { ConfigProvider } from 'antd';
import TaskList from './TaskList';

const App: React.FC = () => {
  return (
    <ConfigProvider>
      <div>
        <h1>任务管理应用</h1>
        <TaskList />
      </div>
    </ConfigProvider>
  );
};

export default App;
// TaskList.tsx
import React, { useState } from 'react';
import { atom, useAtom } from 'jotai';
import { Button, Input, List } from 'antd';

// 创建一个原子,用于管理任务列表的状态
const taskListAtom = atom<string[]>([]);

const TaskList: React.FC = () => {
  const [task, setTask] = useState('');
  const [tasks, setTasks] = useAtom(taskListAtom);

  const addTask = () => {
    if (task.trim() === '') return; // 防止添加空任务
    setTasks([...tasks, task]);
    setTask(''); // 添加后清空输入框
  };

  const deleteTask = (index: number) => {
    const newTasks = tasks.filter((_, i) => i !== index);
    setTasks(newTasks); // 更新任务列表
  };

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
      <h3>任务列表</h3>
      <Input
        value={task}
        onChange={(e) => setTask(e.target.value)}
        placeholder="输入任务"
        style={{ marginBottom: '10px' }}
      />
      <Button type="primary" onClick={addTask} style={{ marginBottom: '20px' }}>
        添加任务
      </Button>
      <List
        bordered
        dataSource={tasks}
        renderItem={(item, index) => (
          <List.Item>
            <span>{item}</span>
            <Button 
              type="link" 
              onClick={() => deleteTask(index)} 
              style={{ marginLeft: 'auto' }}
            >
              删除
            </Button>
          </List.Item>
        )}
      />
    </div>
  );
};

export default TaskList;

加强版 支持持久化读取缓存数据

image.png

这样初始化即可是下面的效果,新增删除功能不变

image.png

// atom.ts
import { atom } from 'jotai'

export const taskListAtom = atom<string[]>([])
export const taskCountAtom = atom((get) => get(taskListAtom).length)
// TaskCount.tsx
import React from 'react';
import { useAtom } from 'jotai';
import { taskCountAtom } from './atom';

const TaskCount: React.FC = () => {
  const [taskCount] = useAtom(taskCountAtom);

  return <h4>当前任务总数: {taskCount}</h4>;
};

export default TaskCount;
// useTaskList.ts 自定义hooks
import { useAtom } from 'jotai'
import { taskListAtom } from './atom'

const useTaskList = () => {
  const [tasks, setTasks] = useAtom<string[]>(taskListAtom) // 显式声明类型

  const addTask = (task: string) => {
    if (task.trim() === '') return
    setTasks((prevTasks) => [...prevTasks, task]) // 这里 prevTasks 会被推断为 string[]
  }

  const deleteTask = (index: number) => {
    setTasks((prevTasks) => prevTasks.filter((_, i) => i !== index))
  }

  return { tasks, addTask, deleteTask }
}

export default useTaskList
// TaskList.tsx
import React, { useState, Suspense } from 'react';
import { Button, Input, List } from 'antd';
import useTaskList from './useTaskList';

// 懒加载计数器组件
const TaskCount = React.lazy(() => import('./TaskCount'));

const TaskList: React.FC = () => {
  const [task, setTask] = useState('');
  const { tasks, addTask, deleteTask } = useTaskList();

  return (
    <div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
      <h3>任务列表</h3>
      <Input
        value={task}
        onChange={(e) => setTask(e.target.value)}
        placeholder="输入任务"
        style={{ marginBottom: '10px' }}
      />
      <Button type="primary" onClick={() => { addTask(task); setTask(''); }}>
        添加任务
      </Button>
      <Suspense fallback={<div>加载中...</div>}>
        <TaskCount />
      </Suspense>
      <List
        bordered
        dataSource={tasks}
        renderItem={(item, index) => (
          <List.Item>
            <span>{item as string}</span>
            <Button 
              type="link" 
              onClick={() => deleteTask(index)} 
              style={{ marginLeft: 'auto' }}
            >
              删除
            </Button>
          </List.Item>
        )}
      />
    </div>
  );
};

export default TaskList;
// App.tsx
import React, { useEffect } from 'react';
import { useAtom } from 'jotai';
import TaskList from './TaskList';
import { taskListAtom } from './atom';

const App: React.FC = () => {
  const [, setTasks] = useAtom(taskListAtom); // 使用 useAtom 获取 setTasks

  // 读取 localStorage 中的任务数据
  useEffect(() => {
    const storedTasks = localStorage.getItem('tasks');
    if (storedTasks) {
      setTasks(JSON.parse(storedTasks)); // 使用 setTasks 更新原子值
    }
  }, [setTasks]);

  return (
    <div>
      <h1>任务列表</h1>
      <TaskList />
    </div>
  );
};

export default App;

或者使用

// App.tsx
import React, { useEffect } from 'react';
import { Provider, useAtom } from 'jotai';
import TaskList from './TaskList';
import { taskListAtom } from './atom';

const App: React.FC = () => {
  const [, setTasks] = useAtom(taskListAtom);

  useEffect(() => {
    // 读取 localStorage 中的任务数据
    const storedTasks = localStorage.getItem('tasks');
    if (storedTasks) {
      setTasks(JSON.parse(storedTasks));
    }
  }, [setTasks]);

  useEffect(() => {
    // 监听localStorage变化
    const handleStorageChange = () => {
      const updatedTasks = JSON.parse(localStorage.getItem('tasks') || '[]');
      setTasks(updatedTasks);
    };

    window.addEventListener('storage', handleStorageChange);
    return () => {
      window.removeEventListener('storage', handleStorageChange);
    };
  }, [setTasks]);

  return (
    <div>
      <h1>任务管理应用</h1>
      <TaskList />
    </div>
  );
};

const Root: React.FC = () => (
  <Provider>
    <App />
  </Provider>
);

export default Root;

这段代码通过jotai实现了一个简单的状态管理逻辑,可以在用户对任务进行操作时(如添加、删除任务)自动更新localStorage并保持同步。这样就能保证用户刷新页面或多个标签页之间的任务数据是一致的。

什么时候用哪个

  • 如果你需要替换 useState+useContext,Jotai 很合适。
  • 如果你想在 React 之外更新状态,Zustand 效果更好。
  • 如果代码拆分很重要,Jotai 应该表现良好。
  • 如果你更喜欢 Redux devtools,Zustand 是个不错的选择。
  • 如果你想使用 Suspense,Jotai 就是其中之一。

Jotai 的优点和缺点

优点

  1. 简单易用

    • Jotai 的 API 非常简洁,使用起来直观,适合对状态管理有基本需求的应用。
  2. 原子状态管理

    • Jotai 采用原子状态的模型,可以将状态拆分成多个独立的原子,这样就可以在不影响其他状态的情况下更新单个状态。
  3. 灵活性

    • 原子可以动态创建,可以根据需要仅在特定组件中使用状态,避免不必要的重渲染。
  4. 性能优化

    • Jotai 只会在相关联的组件重新渲染时触发更新,因而能提供更好的性能表现。
  5. 支持 SSR 和 Hooks

    • Jotai 可以与服务器端渲染(SSR)友好配合,并且通过 React Hooks 提供了良好的集成。
  6. 与 React 生态兼容

    • Jotai 可以与其他 React 库一起使用,如 React Router、React Query 等,具有很好的兼容性。

缺点

  1. 社区和文档

    • 作为一个相对较新的库,Jotai 的社区和生态系统可能没有 Redux 等成熟库那么丰富,对新用户的支持和资源相对较少。
  2. 较小的功能集

    • Jotai 设计为轻量级,不提供如中间件、DevTools 等高级功能,这意味着开发者需要自己处理一些功能。
  3. 学习曲线

    • 在进行大规模应用开发时,虽然简单,但对复杂状态管理的支持不如更复杂的解决方案(如 Redux),可能需要额外的思考。
  4. 缺乏时间旅行

    • 相比于 Redux,Jotai 默认不支持时间旅行(time travel)等调试和开发工具,回滚状态时会比较麻烦。

总结

Jotai 是一个非常灵活且高性能的状态管理工具,适用于简单到中等复杂度的项目。对于大型应用,可能需要评估是否满足需求,以及是否愿意在某些功能上做出妥协。选择使用时,需要结合具体项目的需求来进行权衡。