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;