创建 atom
在 Jotai 中创建一个 atom 是建立原子化状态管理的最基本操作。这项任务主要是通过jotai
库中提供的atom
函数来完成的。在 Jotai 中,atom 的类型一共有四种:
- 原始 atom(Primitive Atom)
- 只读 atom(Read-only Atom)
- 只写 atom(Write-only Atom)
- 读写 atom(Read-Write Atom)
这四种 atom 都是使用atom
函数来创建,其特性只是根据创建的时候传入的构建参数不同而不同。在默认情况下使用atom
函数搭配初始值创建的 atom 都是原始 atom,而剩下的三种 atom 则是使用专用的读写函数来创建,所以这三种 atom 也被称为派生 atom。
原始 atom 的创建
原始 atom 的构建是最简单的,其中除了可以存放数字、布尔、字符串等内容外,存放列表、对象都是可以的。
在以下示例中创建的 atom 都是原始 atom。
import { atom } from "jotai";
const numberAtom = atom<number>(10);
const boolAtom = atom<boolean>(false);
const employeeAtom = atom({ name: "John", position: "Developer" });
可以观察一下这样构建的 atom 的类型,对于示例中的numberAtom
来说,其类型是PrimitiveAtom<number> & WithInitialValue<number>
,这个就代表了创建出来的是一个原始 atom。
创建原始 atom 的atom
函数的签名如下:
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>;
只读 atom 的创建
相对于原始 atom,派生 atom 才是 Jotai 在 React 应用中大量使用的主力。派生 atom 不仅可以将 atom 的读写进行分离,还可以构建不同 atom 之间的依赖关系。派生 atom 中最简单的 atom 构建是只读 atom 的构建。
上文说过,派生 atom 的创建是通过使用专用的读写函数来创建的,只读 atom 的创建就是仅向atom
函数提供一个读取 atom 内容的函数来创建 atom。例如以下创建了一个相对于上面示例中employeeAtom
的只读 atom。
const employeeNameAtom = atom((get) => get(employeeAtom).name);
在这个 atom 的创建中,get
函数是atom
函数提供的,用于在当前 atom 中获取其他 atom 的内容。只读 atom 的创建函数签名如下:
function atom<Value>(read: (get: Getter) => Value): Atom<Value>;
只读 atom 主要用来通过对其他 atom 内容进行计算和处理,获得相关处理结果的功能。当get
中调用的 atom 发生变化的时候,当前的派生 atom 也会相应的发生变化。如果在派生 atom 中多次使用get
依赖了多个不同的 atom,那么只要其中有一个 atom 发生了变化,当前这个派生 atom 都会发生变化。
只写 atom 的创建
创建一个只读的派生 atom 是比较好理解的,但是创建一个只写的派生 atom 就不是那么容易理解了。起码在使用其他状态库的时候,很少出现一个状态只接受写入的。但是在 Jotai 里,派生 atom 既然可以从其他 atom 里派生出来,那么也就应该可以对输入数据进行处理再更新其他的 atom。这个功能就是只写 atom 来完成的。
例如创建一个更新产品信息 atom 中折扣信息的只写 atom。
const productAtom = atom({
code: "0000",
name: "cabbage",
price: 2.39,
discount: 0.1,
});
const discountUpdateAtom = atom(null, (get, set, discount: number) => {
set(productAtom, (prev) => {
prev.discount = discount;
return prev;
});
});
看起来这个示例里的只写 atom 似乎是多此一举。但是在采用了关注点分离设计的项目中,用来完成状态更新的组件就可以只依赖于这个只写 atom,而不必再引入其他的 atom。而且,借助于只写 atom 还可以完成一个结构比较复杂的 atom 中局部内容的更新,这对于简化数据更新的操作也是有好处的。
定义只写 atom 的函数签名看起来会有一点儿复杂,但是没有关系,我们可以结合上面的示例对其进行分析。
function atom<Value, Args extends unknown[], Result>(
read: Value,
write: (get: Getter, set: Setter, ...args: Args) => Result
): WritableAtom<Value, Args, Result>;
首先看函数泛型参数中的Value
,因为这个 atom 的值是不被我们关心的,而且这个 atom 实际上也不会承载什么值,所以基本上Value
一般都是null
。Result
是写入函数的返回值类型,但是需要注意的是,这个写入函数的返回值并不是最终写入 atom 的内容,它只是这个 atom 的写入功能被调用以后返回的一个结果而已。Args
就是需要传入写入函数的需要处理的数据了,需要注意的是写入函数对于传入的多个参数的处理,但是一般在习惯上还是仅传入一个参数,如果需要传入多个数据,可以使用列表或者对象来将其包装起来。
跟只读 atom 一样,只写 atom 也可以同时更新多个 atom 的值,这只需要在写入函数中通过调用set
函数来完成新内容的写入即可。set 函数在更新 atom 值的时候有两种使用方式,一是直接赋予 atom 新值,即set(atom, newValue)
的形式,另一种则是通过更新函数的形式,即set(atom, (prev) => {})
的形式。
读写 atom 的创建
在了解了只读 atom 和只写 atom 之后,读写 atom 的创建就变得十分简单了。读写 atom 就是只读 atom 和只写 atom 的功能的组合。所以先来看创建读写 atom 的函数签名。
function atom<Value, Args extends unknown[], Result>(
read: (get: Getter) => Value,
write: (get: Getter, set: Setter, ...args: Args) => Result
): WritableAtom<Value, Args, Result>;
在这个函数签名中,read
参数已经不是只写 atom 创建函数中的Value
了,而是只读 atom 创建函数中的read
了,这就说明读写 atom 可以通过读函数来通过其他的 atom 产生值,然后在对其进行更新赋值的时候,它还可以更新其他 atom 的内容。
对于读写 atom 这里就不再提供示例了,读者只需要注意读写 atom 是一个十分自由的 atom,它的读函数和写函数分别可以操作不同的 atom,而不必限制其操作读写的 atom 必须一致。
读写 atom 在使用的时候,其特征于原始 atom 非常相像,不过其主要目标是完成更加复杂的操作,所以如果不需要复杂的 atom 更新操作的时候,还是要优先选择使用原始 atom。
生命周期控制
所有类型的 atom 都可以在使用atom
函数完成创建以后为其附加一个onMount
属性,这个属性可以在 atom 被 Provider 订阅的时候自动调用其写入函数来更新其内容,并且可以在其从 Provider 解除订阅的时候执行一些清理操作。
以下是一个非常简单的为 atom 定义onMount
属性的示例。
const numAtom = atom(1);
numAtom.onMount = (set) => {
set((prev) => prev + 1);
return () => {};
};
在这个示例中onMount
函数接受一个参数,用于更新当前 atom 的值,注意这个set
参数与派生 atom 创建中写入函数中提供的Setter
是不一样的。onMount
函数的返回值是一个onUnmount
函数,这个函数用来在 atom 解除订阅的时候执行清理工作。
onMount
在 atom 的内容被订阅的时候执行,如果在代码中只是使用了 atom 的Setter
方法,那么onMount
函数是不会被执行的。