JotaiJotai

状態
原始和灵活的 React 状态管理

创建 Atom

atomWithToggle

atomWithToggle 创建一个新的原子,它以一个布尔值作为初始状态,并使用一个 setter 函数来切换它。

这避免了为了更新第一个原子的状态而必须设置另一个原子的方式。

import { WritableAtom, atom } from "jotai";
export function atomWithToggle(
initialValue?: boolean
): WritableAtom<boolean, boolean | undefined> {
const anAtom = atom(initialValue, (get, set, nextValue?: boolean) => {
const update = nextValue ?? !get(anAtom);
set(anAtom, update);
});
return anAtom as WritableAtom<boolean, boolean | undefined>;
}

可以提供可选的初始状态作为第一个参数。

setter 函数可以有一个可选的参数来强制一个特定的状态,比如如果你想从它中创建一个 setActive 函数。

这是它的使用方法。

import { atomWithToggle } from "XXX";
// 将有一个初始值设置为 true
const isActiveAtom = atomWithToggle(true);

在一个组件中:

const Toggle = () => {
const [isActive, toggle] = useAtom(isActiveAtom);
return (
<>
<button onClick={() => toggle()}>
isActive: {isActive ? "yes" : "no"}
</button>
<button onClick={() => toggle(true)}>force true</button>
<button onClick={() => toggle(false)}>force false</button>
</>
);
};

atomWithToggleAndStorage

atomWithToggleAndStorage 类似于 atomWithToggle,但也可以使用 atomWithStorage

这是来源:

import { WritableAtom, atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
export function atomWithToggleAndStorage(
key: string,
initialValue?: boolean,
storage?: any
): WritableAtom<boolean, boolean | undefined> {
const anAtom = atomWithStorage(key, initialValue, storage);
const derivedAtom = atom(
(get) => get(anAtom),
(get, set, nextValue?: boolean) => {
const update = nextValue ?? !get(anAtom);
set(anAtom, update);
}
);
return derivedAtom;
}

以及它是如何使用的:

import { atomWithToggleAndStorage } from "XXX";
// 将初始值设置为 false 并存储在 localStorage 中的键“isActive”下
const isActiveAtom = atomWithToggleAndStorage("isActive");

在组件中的用法也与 atomWithToggle 相同。

atomWithCompare

atomWithCompare 创建原子,当自定义比较函数 areEqual(prev, next) 为假时触发更新。

这可以通过忽略对您的应用程序无关紧要的状态更改来帮助您避免不需要的 re-render。

注意:Jotai 在内部使用 Object.is 来比较发生变化时的值。 如果 areEqual(a, b) 返回 false,但 Object.is(a, b) 返回 true,则 Jotai 不会触发更新。

import { atomWithReducer } from "jotai/utils";
export function atomWithCompare<Value>(
initialValue: Value,
areEqual: (prev: Value, next: Value) => boolean
) {
return atomWithReducer(initialValue, (prev: Value, next: Value) => {
if (areEqual(prev, next)) {
return prev;
}
return next;
});
}

以下是您如何使用它来创建一个忽略浅相等更新的原子:

import { atomWithCompare } from "XXX";
import { shallowEquals } from "YYY";
import { CSSProperties } from "react";
const styleAtom = atomWithCompare<CSSProperties>(
{ backgroundColor: "blue" },
shallowEquals
);

在一个组件中:

const StylePreview = () => {
const [styles, setStyles] = useAtom(styleAtom);
return (
<div>
<div styles={styles}>Style preview</div>
{/* 单击此按钮两次只会触发一次渲染 */}
<button onClick={() => setStyles({ ...styles, backgroundColor: "red" })}>
Set background to red
</button>
{/* 单击此按钮两次只会触发一次渲染 */}
<button onClick={() => setStyles({ ...styles, fontSize: 32 })}>
Enlarge font
</button>
</div>
);
};

atomWithRefresh

atomWithRefresh 通过使用更新函数创建一个可以强制刷新的派生原子。

当您需要在执行副作用后刷新异步数据时,这很有用。

它还可以用于实现“下拉刷新”功能。

import { atom, Getter } from "jotai";
export function atomWithRefresh<T>(fn: (get: Getter) => T) {
const refreshCounter = atom(0);
return atom(
(get) => {
get(refreshCounter);
return fn(get);
},
(_, set) => set(refreshCounter, (i) => i + 1)
);
}

以下是您将如何使用它来实现可刷新的数据源:

import { atomWithRefresh } from "XXX";
const postsAtom = atomWithRefresh((get) =>
fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json())
);

在一个组件中:

const PostsList = () => {
const [posts, refreshPosts] = useAtom(postsAtom);
return (
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
{/* 单击此按钮将重新获取帖子 */}
<button type="button" onClick={refreshPosts}>
Refresh posts
</button>
</div>
);
};

atomWithListeners

atomWithListeners 创建一个原子和一个 hook。 可以调用 hook 来添加新的 listener。 hook 将回调作为参数,每次设置原子值时都会调用该回调。 该 hook 还返回一个函数来移除 listener。

当您想要创建一个可以监听原子状态何时发生变化的组件时,这会很有用,而不必在每次状态更改时都重新渲染该组件。

import { useEffect } from "react";
import { atom, Getter, Setter, SetStateAction } from "jotai";
import { useUpdateAtom } from "jotai/utils";
type Callback<Value> = (
get: Getter,
set: Setter,
newVal: Value,
prevVal: Value
) => void;
export function atomWithListeners<Value>(initialValue: Value) {
const baseAtom = atom(initialValue);
const listenersAtom = atom(<Callback<Value>[]>[]);
const anAtom = atom(
(get) => get(baseAtom),
(get, set, arg: SetStateAction<Value>) => {
const prevVal = get(baseAtom);
set(baseAtom, arg);
const newVal = get(baseAtom);
get(listenersAtom).forEach((callback) => {
callback(get, set, newVal, prevVal);
});
}
);
const useListener = (callback: Callback<Value>) => {
const setListeners = useUpdateAtom(listenersAtom);
useEffect(() => {
setListeners((prev) => [...prev, callback]);
return () =>
setListeners((prev) => {
const index = prev.indexOf(callback);
return [...prev.slice(0, index), ...prev.slice(index + 1)];
});
}, [setListeners, callback]);
};
return [anAtom, useListener] as const;
}

在一个组件中:

const [countAtom, useCountListener] = atomWithListeners(0);
function EvenCounter() {
const [evenCount, setEvenCount] = useState(0);
useCountListener(
useCallback(
(get, set, newVal, prevVal) => {
// 每次设置 `countAtom` 的值时,我们检查它的新值是否为偶数
// 如果是,我们增加 `evenCount`。
if (newVal % 2 === 0) {
setEvenCount((c) => c + 1);
}
},
[setEvenCount]
)
);
return <>Count was set to an even number {evenCount} times.</>;
}

atomWithBroadcast

atomWithBroadcast 创建一个原子。 原子将在浏览器选项卡和框架之间共享,类似于 atomWithStorage 但具有初始化限制。

当您希望状态在不使用 localStorage 的情况下相互交互并且仅使用 Broadcast Channel API 允许浏览上下文(即窗口、选项卡、框架、创建组件或 iframe)之间进行基本通信时,这可能很有用 和同源的 worker。 根据 MDN 文档,广播不支持在初始化中接收消息,如果我们想要支持,我们可能需要向 atomWithBroadcast 添加额外的东西(比如本地存储)。

import { atom } from "jotai";
export function atomWithBroadcast<Value>(key: string, initialValue: Value) {
const baseAtom = atom(initialValue);
const listeners = new Set<(event: MessageEvent<any>) => void>();
const channel = new BroadcastChannel(key);
channel.onmessage = (event) => {
listeners.forEach((l) => l(event));
};
const broadcastAtom = atom<Value, { isEvent: boolean; value: Value }>(
(get) => get(baseAtom),
(get, set, update) => {
set(baseAtom, update.value);
if (!update.isEvent) {
channel.postMessage(get(baseAtom));
}
}
);
broadcastAtom.onMount = (setAtom) => {
const listener = (event: MessageEvent<any>) => {
setAtom({ isEvent: true, value: event.data });
};
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};
const returnedAtom = atom<Value, Value>(
(get) => get(broadcastAtom),
(get, set, update) => {
set(broadcastAtom, { isEvent: false, value: update });
}
);
return returnedAtom;
}
const broadAtom = atomWithBroadcast("count", 0);
const ListOfThings = () => {
const [count, setCount] = useAtom(broadAtom);
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};

atomWithDebounce

atomWithDebounce 创建一个防抖状态集的原子。

此 utils 对于文本搜索输入很有用,您希望在等待一段时间后 仅调用一次派生原子中的函数,而不是在每次击键时触发操作。

import { atom, SetStateAction } from "jotai";
export default function atomWithDebounce<T>(
initialValue: T,
delayMilliseconds = 500,
shouldDebounceOnReset = false
) {
const prevTimeoutAtom = atom<ReturnType<typeof setTimeout> | undefined>(
undefined
);
// 不要导出 currentValueAtom,因为使用此原子设置状态会导致 currentValueAtom 和 debouncedValueAtom 之间的状态不一致
const _currentValueAtom = atom(initialValue);
const isDebouncingAtom = atom(false);
const debouncedValueAtom = atom(
initialValue,
(get, set, update: SetStateAction<T>) => {
clearTimeout(get(prevTimeoutAtom));
const prevValue = get(_currentValueAtom);
const nextValue =
typeof update === "function"
? (update as (prev: T) => T)(prevValue)
: update;
const onDebounceStart = () => {
set(_currentValueAtom, nextValue);
set(isDebouncingAtom, true);
};
const onDebounceEnd = () => {
set(debouncedValueAtom, nextValue);
set(isDebouncingAtom, false);
};
onDebounceStart();
if (!shouldDebounceOnReset && nextValue === initialValue) {
onDebounceEnd();
return;
}
const nextTimeoutId = setTimeout(() => {
onDebounceEnd();
}, delayMilliseconds);
// 设置上一个超时原子以防需要清除
set(prevTimeoutAtom, nextTimeoutId);
}
);
// 导出原子 setter 以在需要时清除超时
const clearTimeoutAtom = atom(null, (get, set, _arg) => {
clearTimeout(get(prevTimeoutAtom));
set(isDebouncingAtom, false);
});
return {
currentValueAtom: atom((get) => get(_currentValueAtom)),
isDebouncingAtom,
clearTimeoutAtom,
debouncedValueAtom,
};
}

警告

请注意,此原子与 React 18 中的并发功能(例如 useTransitionuseDeferredValue )具有不同的目标,它们的主要目的是防止阻塞与页面的交互以进行昂贵的更新。

有关详细信息,请阅读标题为“它与 setTimeout 有何不同?”部分下的此 github 讨论 https://github.com/reactwg/react-18/discussions/41

示例

下面的沙箱链接显示了我们如何使用派生原子根据 debouncedValueAtom 的值获取状态。

当在 <SearchInput> 中输入一个 pokemon 的名字时,我们不会对每个字母发送 get 请求,而是仅在自上次输入文本后经过 delayMilliseconds 之后。

这减少了对服务器的后端请求数。