JotaiJotai

状態
Primitive and flexible state management for React

组合 atom

库提供的atom函数非常原始, 但它也非常灵活,你可以组合多个原子 实现一个功能。

再次注意,atom() 创建了一个原子配置,它是一个对象 定义原子的行为。

让我们回顾一下我们如何推导出一个原子。

基本派生原子

这是派生原子的最简单示例之一:

export const textAtom = atom("hello");
export const textLenAtom = atom((get) => get(textAtom).length);

textLenAtom 被称为只读原子,因为 它没有定义 write 函数。

下面是另一个带有 write 函数的简单示例:

const textAtom = atom("hello");
export const textUpperCaseAtom = atom(
(get) => get(textAtom).toUpperCase(),
(_get, set, newText) => set(textAtom, newText)
);

在这种情况下,textUpperCaseAtom 能够设置原始的 textAtom。 所以,我们只能导出 textUpperCaseAtom ,可以在更小的范围内隐藏 textAtom 。

现在,让我们看一些真实的例子。

覆盖默认原子值

假设我们有一个只读原子。 显然只读原子是不可写的,但是我们可以组合两个原子来覆盖只读原子的值。

const rawNumberAtom = atom(10.1); // can be exported
const roundNumberAtom = atom((get) => Math.round(get(rawNumberAtom)));
const overwrittenAtom = atom(null);
export const numberAtom = atom(
(get) => get(overwrittenAtom) ?? get(roundNumberAtom),
(get, set, newValue) => {
const nextValue =
typeof newValue === "function" ? newValue(get(numberAtom)) : newValue;
set(overwrittenAtom, nextValue);
}
);

最终的“numberAtom”就像一个普通的原始原子,如“atom(10)”。 如果你设置一个数字值,它将覆盖 overwrittenAtom 值, 如果您设置“null”,它将是“roundNumberAtom”值。

可重用的实现在 jotai/utils 中作为 atomWithDefault 可用。 请参阅 atomWithDefault

接下来,让我们看另一个与外部值同步的示例。

将原子值与外部值同步

我们要处理一些外部值。 localStorage 就是其中之一。 另一个是 window.title

让我们看看如何创建一个与 localStorage 同步的原子。

const baseAtom = atom(localStorage.getItem("mykey") || "");
export const persistedAtom = atom(
(get) => get(baseAtom),
(get, set, newValue) => {
const nextValue =
typeof newValue === "function" ? newValue(get(baseAtom)) : newValue;
set(baseAtom, nextValue);
localStorage.setItem("mykey", nextValue);
}
);

persistedAtom 像原始原子一样工作,但它的值保存在 localStorage 中。

可重用的实现在 jotai/utils 中作为 atomWithStorage 可用。 请参阅 atomWithStorage

这种用法有一个警告。 虽然原子配置不保存值,但外部值是单例值。 所以,如果我们在两个不同的 Provider 中使用这个 atom,就会出现两个 persistedAtom 值不一致的情况。 如果外部值具有订阅机制,则可以解决此问题。

比如jotai-valtio中的atomWithProxy是自带订阅的,所以我们没有这样的限制。 不同 Provider 中的值将是同步的。

回到主题,让我们探讨另一个例子。

使用 atomWith* 工具扩展原子

我们有几个名称以 atomWith 开头的实用程序。 他们创建具有特定功能的原子。 不幸的是,我们不能组合两个原子工具。 例如,atomWithStorageatomWithReducer 不能用于定义单个原子。

在这种情况下,我们需要自己推导出一个原子。 让我们尝试向 atomWithStorage 添加 reducer 功能:

const reducer = ...
const baseAtom = atomWithStorage('mykey', '')
export const derivedAtom = atom(
(get) => get(baseAtom),
(get, set, action) => {
set(baseAtom, reducer(get(baseAtom), action))
}
)

这很容易,因为在这种情况下,与 atomWithStorage 相比,atomWithReducer 是一个简单的实现。

对于更复杂的情况,这不会很容易。 它仍然是一个开放的研究领域。

最后,让我们看另一个带有动作的例子。

Action 原子

这应该是 README 中描述的已知模式。 尽管如此,重新访问它可能会有用。

让我们创建一个只能递增或递减 1 的计数器。

一种解决方案是 atomWithReducer

const countAtom = atomWithReducer(0, (prev, action) => {
if (action === "INC") {
return prev + 1;
}
if (action === "DEC") {
return prev - 1;
}
throw new Error("unknown action");
});

这很好,但不是很原子。 如果我们想从代码 拆分/延迟 加载中获益, 我们想创建只写原子,或 action 原子。

const baseAtom = atom(0); // do not export
export const countAtom = atom((get) => get(baseAtom)); // read only
export const incAtom = atom(null, (_get, set) => {
set(baseAtom, (prev) => prev + 1);
});
export const decAtom = atom(null, (_get, set) => {
set(baseAtom, (prev) => prev - 1);
});

这更原子化,看起来像 Jotai 方式。

您还可以创建一个将调用另一个 action 原子的 action 原子:

// 从前面的代码继续
export const dispatchAtom = atom(null, (_get, set, action) => {
if (action === 'INC') {
set(incAtom)
}
if (action === 'DEC') {
set(decAtom)
}
throw new Error('unknown action')
}

我们为什么要它? 因为只有在需要的时候才会用到。 它允许代码拆分和无用代码消除。

总之

原子是积木。 通过基于其他原子组成原子, 我们可以实现复杂的逻辑。 这不仅适用于读派生原子,也适用于写操作原子。

本质上,原子就像函数,所以组合原子就像组合函数和其他函数。

注意:我们提到我们的原子可以包含任何类型的数据,它可以是字符串、Blob、Observer,任何东西。 只有一个例外。 因为派生原子是使用函数定义的,如果我们向它传递一个不完全是纯 getter 的函数,Jotai 将无法理解。 所以你可以做的就是简单地将你的函数包装在一个对象中。

const doublerAtom = atom({ callback: (n) => n * 2 });
// Usage
const [doubler] = useAtom(doublerAtom);
const doubledValue = doubler.callback(50); // Will compute to 100