JotaiJotai

状態
原始和灵活的 React 状态管理

核心

基础 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 配置可以在任何地方创建,但引用相等很重要。 它们也可以动态创建。 要在渲染函数中创建原子,需要使用 useMemouseRef 来获得稳定的引用。 如果对使用 useMemouseRef 进行 memoization 有疑问,请使用 useMemo

const Component = ({ value }) => {
const valueAtom = useMemo(() => atom({ value }), [value]);
// ...
};

签名

// 原始 atom
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>;
// 只读 atom
function atom<Value>(
read: (get: Getter) => Value | Promise<Value>
): Atom<Value>;
// 可写派生 atom
function atom<Value, Update>(
read: (get: Getter) => Value | Promise<Value>,
write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>;
// 只写派生 atom
function 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.useStatesetState

debugLabel 属性

创建的原子配置可以有一个可选属性 debugLabel。 调试标签用于在调试中显示原子。 有关详细信息,请参阅 调试指南

注意:虽然调试标签不必是唯一的,但通常建议使它们易于区分。

onMount 属性

创建的原子配置可以有一个可选属性 onMountonMount 是一个接受函数 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 依赖于 textAtomtextAtom 依赖于 uppercaseAtom。 因此,将 uppercaseAtom 添加到 textAtom 的依赖项中。 当我们重新运行读取函数时(因为它的依赖项 textAtom 已更新),依赖项会再次创建,在本例中也是如此。 然后我们删除陈旧的依赖项并替换为最新的依赖项。

原子可以按需创建

虽然这里的基本示例显示了在组件外部全局定义原子,但对于我们可以在何处或何时创建原子没有任何限制。 只要我们记得原子是由它们的对象引用身份标识的,我们就可以随时创建它们。

如果您在渲染函数中创建原子,您通常希望使用像 useRefuseMemo 这样的 hook 来进行 memoization。 否则,每次组件渲染时都会重新创建原子。

您可以创建一个原子并将其存储在 useState 中,甚至可以存储在另一个原子中。 请参阅问题 #5 中的示例。

您可以在全局某处缓存原子。 请参阅此示例那个例子

检查参数化原子的 utils 中的 atomFamily

额外 APIs

Provider

Provider 组件是为组件子树提供状态。 多个 Provider 可以用于多个子树,甚至可以嵌套。 这就像 React 上下文一样工作。

如果在没有 Provider 的树中使用原子,它将使用默认状态。 这就是所谓的无提供者模式。

Provider 之所以有用,有以下三个原因:

  1. 为每个子树提供不同的状态。
  2. 接受原子的初始值。
  3. 通过重新安装清除所有原子。
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 = () => (
<Provider
initialValues={[
[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 更新)。