组合原子
库提供的 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) // 可以被导出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)) : newValueset(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)) : newValueset(baseAtom, nextValue)localStorage.setItem('mykey', nextValue)},)
persistedAtom
的工作方式就像一个原始原子,但它的值
被持久化在 localStorage
中。
可重用的实现在 jotai/utils
中作为 atomWithStorage
可用。参见 atomWithStorage。
这种用法有一个注意事项。虽然原子配置不持有值,
但外部值是一个单例值。
所以,如果我们在两个不同的 Providers 中使用这个原子,
两个 persistedAtom
的值之间会有不一致。
如果外部值有订阅机制,这个问题就可以解决。
例如,jotai-valtio
中的 atomWithProxy
带有订阅,
所以我们没有这样的限制。在不同的 Providers 中的值
将保持同步。
回到主题,让我们探索另一个例子。
使用 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))})
这很简单,因为在这种情况下,atomWithReducer
相比 atomWithStorage
是一个简单的实现。
对于更复杂的情况,这可能不会很容易。 这仍然是一个开放的研究领域。
最后,让我们看另一个带有操作的例子。
操作原子
这应该是一个已知的模式,因为它在 README 中有描述。 尽管如此,重新审视可能会有所帮助。
让我们创建一个计数器,你只能增加或减少一。
一个解决方案是 atomWithReducer
:
const countAtom = atomWithReducer(0, (prev, action) => {if (action === 'INC') {return prev + 1}if (action === 'DEC') {return prev - 1}throw new Error('unknown action')})
这很好,但不是非常原子化。 如果我们想从代码分割 / 惰性加载中获益, 我们希望创建只写原子,或者操作原子。
const baseAtom = atom(0) // 不导出export const countAtom = atom((get) => get(baseAtom)) // 只读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 的方式。
你也可以创建一个操作原子,它将调用另一个操作原子:
// 继续上面的代码export const dispatchAtom = atom(null, (_get, set, action) => {if (action === 'INC') {set(incAtom)} else if (action === 'DEC') {set(decAtom)} else {throw new Error('unknown action')}})
为什么我们需要它?因为它只在需要时使用。 它允许代码分割和死代码消除。
总结
原子是构建块。 通过基于其他原子组合原子, 我们可以实现复杂的逻辑。 这不仅适用于读取派生的原子,也适用于写入操作的原子。
本质上,原子就像函数,所以组合原子就像 用其他函数组合函数。
注意:我们提到我们的原子可以包含任何类型的数据,它可以是 字符串,Blob,Observer,真的可以是任何东西。只有一个例外。因为派生原子是使用函数定义的,如果我们传递的函数不是纯 getter,Jotai 将无法理解。 所以你可以做的就是简单地将你的函数包装在一个对象中。
const doublerAtom = atom({ callback: (n) => n * 2 })// 使用const [doubler] = useAtom(doublerAtom)const doubledValue = doubler.callback(50) // 将计算为 100