JotaiJotai

Jotai
React 原始而灵活的状态管理

组合原子

库提供的 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)) : 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

这种用法有一个注意事项。虽然原子配置不持有值, 但外部值是一个单例值。 所以,如果我们在两个不同的 Providers 中使用这个原子, 两个 persistedAtom 的值之间会有不一致。 如果外部值有订阅机制,这个问题就可以解决。

例如,jotai-valtio 中的 atomWithProxy 带有订阅, 所以我们没有这样的限制。在不同的 Providers 中的值 将保持同步。

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

使用 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))
}
)

这很简单,因为在这种情况下,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