核心
基础 APIs
atom
atom
功能是创建一个原子配置。
我们称它为“原子配置”,因为它只是一个定义,还没有任何值。
如果上下文清楚,我们也可以将其称为“原子”。
原子配置是一个不可变的对象。 原子配置对象没有值。 原子值存在于 store 中。
要创建原始原子 (config),您只需提供一个初始值即可。
import { atom } from "jotai";const priceAtom = atom(10);const messageAtom = atom("hello");const productAtom = atom({ id: 12, name: "good stuff" });
您还可以创建派生原子。 我们有三种模式:
- 只读 atom
- 只写 atom
- 读写 atom
为了创建派生原子,我们传递了一个读取函数和一个可选的写入函数。
const readOnlyAtom = atom((get) => get(priceAtom) * 2);const writeOnlyAtom = atom(null, // 为第一个参数传递 `null` 是一种惯例(get, set, update) => {// `update` 是我们收到的用于更新此原子的任何单个值set(priceAtom, get(priceAtom) - update.discount);});const readWriteAtom = atom((get) => get(priceAtom) * 2,(get, set, newPrice) => {set(priceAtom, newPrice / 2);// 您可以同时设置任意数量的原子});
read 函数中的 get
是读取原子值。
它是响应 式的,并且会跟踪读取依赖项。
write 函数中的 get
也是读取 atom 的值,但是没有被跟踪。
此外,它无法读取 Jotai v1 API 中未解析的异步值。
对于异步行为,请参阅 async 文档。
write 函数中的set
是写入原子值。
它将调用目标原子的写入函数。
注意:Atom 配置可以在任何地方创建,但引用相等很重要。
它们也可以动态创建。
要在渲染函数中创建原子,需要使用 useMemo
或 useRef
来获得稳定的引用。 如果对使用 useMemo
或 useRef
进行 memoization 有疑问,请使用 useMemo
。
const Component = ({ value }) => {const valueAtom = useMemo(() => atom({ value }), [value]);// ...};
签名
// 原始 atomfunction atom<Value>(initialValue: Value): PrimitiveAtom<Value>;// 只读 atomfunction atom<Value>(read: (get: Getter) => Value | Promise<Value>): Atom<Value>;// 可写派生 atomfunction atom<Value, Update>(read: (get: Getter) => Value | Promise<Value>,write: (get: Getter, set: Setter, update: Update) => void | Promise<void>): WritableAtom<Value, Update>;// 只写派生 atomfunction atom<Value, Update>(read: Value,write: (get: Getter, set: Setter, update: Update) => void | Promise<void>): WritableAtom<Value, Update>;
initialValue
:原子将返回的初始值,直到它的值被改变。read
:在每次重新渲染时调用的函数。read
的签名是(get) => Value | Promise<Value>
和get
是一个函数,它接受一个原子配置并返回其存储在 Provider 中的值,如下所述。 跟踪依赖关 系,因此如果对原子至少使用一次get
,则每当原子值更改时都会重新取值read
。write
:主要用于改变原子值的函数,以便更好地描述; 每当我们调用返回的useAtom
对的第二个值,即useAtom()[1]
时,它就会被调用。 原始原子中此函数的默认值将更改该原子的值。write
的签名是(get, set, update) => void | Promise<void>
。get
与上面描述的类似,但它不跟踪依赖关系。set
是一个函数,它接受一个原子配置和一个新值,然后更新 Provider 中的原子值。update
是我们从下面描述的useAtom
返回的更新函数中接收到的任意值。
const primitiveAtom = atom(initialValue);const derivedAtomWithRead = atom(read);const derivedAtomWithReadWrite = atom(read, write);const derivedAtomWithWriteOnly = atom(null, write);
有两种原子:可写原子和只读原子。 原始原子总是可写的。 如果指定了 write
,派生原子是可写的。 原始原子的 write
相当于 React.useState
的 setState
。
debugLabel
属性
创建的原子配置可以有一个可选属性 debugLabel
。 调试标签用于在调试中显示原子。 有关详细信息,请参阅 调试指南。
注意:虽然调试标签不必是唯一的,但通常建议使它们易于区分。
onMount
属性
创建的原子配置可以有一个可选属性 onMount
。 onMount
是一个接受函数 setAtom
并可选地返回 onUnmount
函数的函数。
onMount
函数在 atom 首次在提供者中使用时被调用,而 onUnmount
在不再使用时被调用。 在一些边缘情况下,一个原子可以被卸载然后立即安装。
const anAtom = atom(1)anAtom.onMount = (setAtom) => {console.log('atom is mounted in provider')setAtom(c => c + 1) // 增加挂载计数return () => { ... } // 返回可选的 onUnmount 函数}
调用 setAtom
函数将调用原子的 write
。 自定义 write
允许改变行为。
const countAtom = atom(1);const derivedAtom = atom((get) => get(countAtom),(get, set, action) => {if (action.type === "init") {set(countAtom, 10);} else if (action.type === "inc") {set(countAtom, (c) => c + 1);}});derivedAtom.onMount = (setAtom) => {setAtom({ type: "init" });};
useAtom
useAtom
hook 用于读取状态中的原子值。
状态可以看作是原子配置和原子值的 WeakMap。
useAtom
hook 以元组形式返回原子值和更新函数,
就像 React 的 useState
一样。
它需要一个使用 atom()
创建的原子配置。
最初,没有与原子关联的值。
只有通过 useAtom
使用原子后,初始值才会存储在状态中。
如果原子是派生原子,则调用读取函数来计算初始值。
当一个原子不再被使用时,意味着所有使用它的组件都被卸载,并且原子配置不再存在,状态中的值被垃圾收集。
const [value, setValue] = useAtom(anAtom);
setValue
只接受一个参数,它将被传递给原子的 write 函数的第三个参数。
行为取决于写入函数的实现方式。
注意:如 atom 部分所述,您必须注意处理原子的引用,否则它可能会进入无限循环
const stableAtom = atom(0);const Component = () => {const [atomValue] = useAtom(atom(0)); // 这将导致无限循环const [atomValue] = useAtom(stableAtom); // 这可以const [derivedAtomValue] = useAtom(useMemo(// 这也可以() => atom((get) => get(stableAtom) * 2),[]));};
注意:请记住,React 负责调用您的组件。 这意味着它必须是幂等的,可以被多次调用。 即使没有 props 或 atoms 发生变化,你也会经常看到额外的重新渲染。 没有提交的额外重新渲染是预期的行为。 这实际上是 React 18 中 useReducer 的默认行为。
签名
// 原始或可写派生原子function useAtom<Value, Update>(atom: WritableAtom<Value, Update>,scope?: Scope): [Value, SetAtom<Update>];// 只读原子function useAtom<Value>(atom: Atom<Value>, scope?: Scope): [Value, never];
useAtom hook 是读取 Provider 中存储的一个原子值。 它以元组的形式返回原子值和更新函数,就像 useState
一样。 它需要一个使用 atom()
创建的原子配置。 最初,Provider 中没有存储任何值。 第一次通过 useAtom
使用 atom 时,它会在 Provider 中添加一个初始值。 如果原子是派生原子,则执行读取函数以计算初始值。 当一个原子不再被使用时,意味着使用它的所有组件都被卸载,并且原子配置不再存在,该值将从 Provider 中删除。
const [value, setValue] = useAtom(anAtom);
setValue
接受一个参数,该参数将传递给原子的 writeFunction 的第三个参数。 行为取决于 writeFunction 的实现方式。
笔记
原子依赖性如何工作
首先,让我们解释一下。 在当前的实现中,每次调用 "read" 函数时,我们都会刷新依赖项和从属项。 例如,如果 A 依赖于 B,则意味着 B 是 A 的依赖项,A 是 B 的依赖项。
const uppercaseAtom = atom((get) => get(textAtom).toUpperCase());
读取函数是原子的第一个参数。
依赖最初是空的。 第一次使用时,我们运行读取函数并知道 uppercaseAtom
依赖于 textAtom
。 textAtom
依赖于 uppercaseAtom
。 因此,将 uppercaseAtom
添加到 textAtom
的依赖项中。
当我们重新运行读取函数时(因为它的依赖项 textAtom
已更新),依赖项会再次创建,在本例中也是如此。 然后我们删除陈旧的依赖项并替换为最新的依赖项。
原子可以按需创建
虽然这里的基本示例显示了在组件外部全局定义原子,但对于我们可以在何处或何时创建原子没有任何限制。 只要我们记得原子是由它们的对象引用身份标识的,我们就可以随时创建它们。
如果您在渲染函数中创建原子,您通常希望使用像 useRef
或 useMemo
这样的 hook 来进行 memoization。 否则,每次组件渲染时都会重新创建原子。
您可以创建一个原子并将其存储在 useState
中,甚至可以存储在另一个原子中。
请参阅问题 #5 中的示例。
检查参数化原子的 utils 中的 atomFamily
。
额外 APIs
Provider
Provider
组件是为组件子树提供状态。
多个 Provider 可以用于多个子树,甚至可以嵌套。
这就像 React 上下文一样工作。
如果在没有 Provider 的树中使用原子,它将使用默认状态。 这就是所谓的无提供者模式。
Provider 之所以有用,有以下三个原因:
- 为每个子树提供不同的状态。
- 接受原子的初始值。
- 通过重新安装清除所有原子。
const SubTree = () => (<Provider><Child /></Provider>);
签名
const Provider: React.FC<{initialValues?: Iterable<readonly [AnyAtom, unknown]>;scope?: Scope;}>;
Atom 配置不保存值。 原子值驻留在单独的存储中。 Provider 是一个组件,它包含一个存储并在组件树下提供原子值。 Provider 的工作方式类似于 React context provider。 如果您不使用提 Provider,它会以无 Provider 模式使用默认 store 。 如果我们需要为不同的组件树保存不同的原子值,则需要提供者。 Provider 还具有下面描述的一些功能,这些功能在 无 Provider 模式下不存在。
const Root = () => (<Provider><App /></Provider>);
initialValues
属性
Provider 接受一个可选的 prop initialValues
,您可以使用它指定一些初始原子值。
其用例是测试和服务器端渲染。
示例
const TestRoot = () => (<ProviderinitialValues={[[atom1, 1],[atom2, "b"],]}><Component /></Provider>);
TypeScript
initialValues
属性不是类型友好的。
我们可以通过使用辅助函数来缓解它。
const createInitialValues = () => {const initialValues: (readonly [Atom<unknown>, unknown])[] = [];const get = () => initialValues;const set = <Value>(anAtom: Atom<Value>, value: Value) => {initialValues.push([anAtom, value]);};return { get, set };};
scope
属性
Provider 接受一个可选的 prop scope
,您可以将其用于范围内的 Provider。
当使用具有 scope 的原子时,使用具有相同作用域的 Provider。
scope 值的建议是一个独特的符号。
scope 的主要使用在库中。
示例
const myScope = Symbol();const anAtom = atom("");const LibraryComponent = () => {const [value, setValue] = useAtom(anAtom, myScope);// ...};const LibraryRoot = ({ children }) => (<Provider scope={myScope}>{children}</Provider>);
useSetAtom
const switchAtom = atom(false);const SetTrueButton = () => {const setCount = useSetAtom(switchAtom);const setTrue = () => setCount(true);return (<div><button onClick={setTrue}>Set True</button></div>);};const SetFalseButton = () => {const setCount = useSetAtom(switchAtom);const setFalse = () => setCount(false);return (<div><button onClick={setFalse}>Set False</button></div>);};export default function App() {const state = useAtomValue(switchAtom);return (<div>State: <b>{state.toString()}</b><SetTrueButton /><SetFalseButton /></div>);}
如果你需要在不读取原子的情况下更新它的值,你可以使用 useSetAtom()
。
当性能是一个问题时,这特别有用,因为 const [, setValue] = useAtom(valueAtom)
会在每次 valueAtom
更新时导致不必要的重新渲染。
useAtomValue
const countAtom = atom(0);const Counter = () => {const setCount = useSetAtom(countAtom);const count = useAtomValue(countAtom);return (<><div>count: {count}</div><button onClick={() => setCount(count + 1)}>+1</button></>);};
与 useSetAtom
hook 类似,useAtomValue
允许您访问只读原子。
关于原子的更多注释
- 如果您创建一个原始原子,它将使用预定义的 读/写 函数来模拟
useState
行为。 - 如果你创建一个具有 读/写 功能的原子,它们可以提供任何行为,但有一些限制,如下所示。
read
函数将在 React 渲染阶段调用,因此该函数必须是纯函数。 此处 描述了 React 中的纯内容。write
函数将在您最初调用的地方调用,并在 useEffect 中调用以进行后续调用。 所以,你不应该在渲染中调用write
。- 当一个原子最初使用
useAtom
时,它会调用read
函数来获取初始值,这是递归过程。 如果 Provider 中存在原子值,它将被使用而不是调用read
函数。 - 一旦使用了一个原子(并存储在 Provider 中),它的值只有在更新其依赖项时才会更新(包括直接使用 useAtom 更新)。