组合 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 exportedconst 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
开头的实用程序。
他们创建具有特定功能的原子。
不幸的是,我们不能组合两个原子工具。
例如,atomWithStorage
和 atomWithReducer
不能用于定义单个原子。
在这种情况下,我们需 要自己推导出一个原子。
让我们尝试向 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 exportexport const countAtom = atom((get) => get(baseAtom)); // read onlyexport 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 });// Usageconst [doubler] = useAtom(doublerAtom);const doubledValue = doubler.callback(50); // Will compute to 100