了解Jotai

110 阅读2分钟

atom.ts

import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

// —— 存储动漫列表的原子 ——
// animeAtom 中保存了一个对象数组,每个对象包含:
//   title   动画名称(字符串)
//   year    上映年份(数字)
//   watched 是否已观看(布尔值)
export const animeAtom = atom([
  {
    title: 'Ghost in the Shell',
    year: 1995,
    watched: true,
  },
  {
    title: 'Serial Experiments Lain',
    year: 1998,
    watched: false,
  },
]);

// —— 计算已观看比例的派生原子 ——
// processAtom 是一个只读原子(read-only atom),
// 它通过 get(animeAtom) 读取 animeAtom 的当前值,
// 并过滤出 watched 为 true 的条目,最后用已观看数量除以总条目数,
// 得到一个 0~1 之间的小数,表示观看进度。
export const processAtom = atom((get) => {
  const anime = get(animeAtom);
  // 防止数组为空导致除以 0 的情况
  if (anime.length === 0) return 0;
  return anime.filter((item) => item.watched).length / anime.length;
});

// —— 文本原子 ——
// textAtom 存储一个简单的字符串,初始值为 'Hello'
export const textAtom = atom('Hello');

// —— 文本大写派生原子 ——
// upperTextAtom 也是只读原子,
// 通过 get(textAtom) 获取 textAtom 的值后调用 toUpperCase()
// 将其转换为大写字符串。
export const upperTextAtom = atom((get) => get(textAtom).toUpperCase());

// —— 带存储同步的模式原子 ——
// modeAtom 使用 atomWithStorage,使其值与浏览器的 localStorage 同步:
//   Key:    'mode'
//   Default: 'dark'
// 这样刷新页面后,mode 的值(如 'light' 或 'dark')可以持久保留。
export const modeAtom = atomWithStorage('mode', 'dark');

App.tsx

import { Button, Flex, Input } from 'antd';
import { useAtom } from 'jotai';
import { animeAtom, modeAtom, processAtom, textAtom, upperTextAtom } from './atoms';

const App = () => {
  // 读取并设置动漫列表状态(数组),对应 atoms.js 中的 animeAtom
  const [anime, setAnime] = useAtom(animeAtom);
  // 读取已观看比例,只读状态,来自 processAtom 派生原子
  const [process] = useAtom(processAtom);
  // 读取并设置文本状态,来自 textAtom
  const [text, setText] = useAtom(textAtom);
  // 读取大写文本,只读状态,来自 upperTextAtom
  const [upperText] = useAtom(upperTextAtom);
  // 读取并设置模式('dark' 或 'light'),自动同步到 localStorage
  const [mode, setMode] = useAtom(modeAtom);

  // 添加一条新动漫到列表的方法,使用 setAnime 更新状态
  const addAnime = () => {
    setAnime((current) => [...current, { title: 'Big Ear Tutu', year: 2003, watched: true }]);
  };

  return (
    // Flex 容器组件,垂直排列子元素,间距为 10px
    <Flex vertical gap={10}>
      {/* 显示观看进度百分比 */}
      <h2>process:</h2>
      <div>{Math.trunc(process * 100)} %</div>

      {/* 列表展示当前所有动漫的标题 */}
      <ul>
        {anime.map((item) => (
          <li key={item.title}>{item.title}</li>
        ))}
      </ul>

      {/* 添加按钮,点击后调用 addAnime 方法 */}
      <Button onClick={addAnime} style={{ width: 100 }}>
        Add
      </Button>

      {/* 文本输入框,双向绑定 textAtom,输入时更新状态 */}
      <Input value={text} onChange={(e) => setText(e.target.value)} style={{ width: 200 }} />

      {/* 显示 textAtom 转换后的大写文本 */}
      <h2>UpperCase:{upperText}</h2>

      {/* 切换模式按钮,依据当前 mode 切换 dark/light 并持久化 */}
      <Button onClick={() => setMode((prev) => (prev === 'dark' ? 'light' : 'dark'))} style={{ width: 100 }}>
        切换
      </Button>

      {/* 显示当前模式 */}
      <h2>mode:{mode}</h2>
    </Flex>
  );
};

export default App;