跳至主要内容

同步 Atom 效应 - syncEffect()

syncEffect() 是一个 Atom 效应,用于标记应同步的 Atom,并使用外部存储初始化其值。唯一必需的选项是用于输入验证的 refineitemKey 选项允许您为外部存储中的此特定 Atom 指定一个键。如果未指定,它默认为 Atom 自己的键。还可以提供 storeKey 以匹配要同步的外部存储,如果您有多个存储的话。还有其他选项,例如用于更高级情况的 readwrite

输入验证

为了验证来自外部系统的输入并从 mixed 精炼到强类型化的 Flow 或 TypeScript 输入,recoil-sync 使用 Refine 库。该库使用一组可组合的函数来描述类型并执行运行时验证。syncEffect()refine 属性接受一个 Refine Checker。Refine 检查器的类型必须与 Atom 的类型匹配。

简单字符串 Atom 的示例效应

  syncEffect({ refine: string() }),

可空数字的示例效应

  syncEffect({ refine: nullable(number()) }),

自定义用户类

  syncEffect({ refine: custom(x => x instanceof MyClass ? x : null) }),

更复杂的示例

  syncEffect({ refine: object({
id: number(),
friends: array(number()),
positions: dict(tuple(bool(), number())),
})}),

有关详细信息,请参阅 Refine 文档

项目和存储键

itemKey 指定一个唯一键来标识存储的项目,如果未指定,它默认为 Atom 的键。如果使用自定义 read()write(),则它可以覆盖项目键以 升级 或使用 多个项目键

storeKey 可用于指定要同步的外部存储。它应该与相应 <RecoilSync>storeKey 匹配。这在 升级 或存在 多个存储 时很有用。

atom({
key: 'AtomKey',
effects: [
syncEffect({
itemKey: 'myItem',
storeKey: 'storeA',
refine: string(),
}),
],
});

Atom 族

atomFamily 中的 Atom 也可以通过 syncEffect() 同步。族中的每个单独 Atom 都被视为要同步的单独项目。默认的项目键将包含族参数的序列化。如果您指定自己的 itemKey,那么您也应该对族参数进行编码以唯一地标识每个 Atom;可以通过使用 Atom 族 effects 选项的回调来获取参数。

atomFamily({
key: 'AtomKey',
effects: param => [
syncEffect({
itemKey: `myItem-${param}`,
storeKey: 'storeA',
refine: string(),
}),
],
});

向后兼容性

支持遗留系统或使用先前版本的 state 的外部系统可能很重要。为此提供了多种机制。

升级 Atom 类型

如果 Atom 已持久保存到存储中,并且您随后更改了 Atom 的类型,您可以使用 Refine 的 match()asType() 来升级类型。此示例读取当前为数字但以前存储为字符串或对象的 ID。它将升级以前的类型,并且 Atom 将始终存储最新的类型。

const myAtom = atom<number>({
key: 'MyAtom',
default: 0,
effects: [
syncEffect({ refine: match(
number(),
asType(string(), x => parseInt(x)),
asType(object({value: number()}), x => x.value)),
}),
],
});

升级 Atom 键

Atom 的键也可能随着时间的推移而发生变化。read 选项允许我们指定如何从外部存储中读取 Atom。

const myAtom = atom<number>({
key: 'MyAtom',
default: 0,
effects: [
syncEffect({
itemKey: 'new_key',
read: ({read}) => read('new_key') ?? read('old_key'),
}),
],
});

读取时更复杂的转换是可能的,请参见下文。

升级 Atom 存储

您还可以将 Atom 迁移到使用多个效应同步到新的外部存储。

const myAtom = atom<number>({
key: 'MyAtom',
default: 0,
effects: [
syncEffect({ storeKey: 'old_store', refine: number() }),
syncEffect({ storeKey: 'new_store', refine: number() }),
],
});

与多个存储同步

对于 Atom 始终与多个存储系统同步可能是可取的。例如,某个 UI 状态的 Atom 可能希望为可共享 URL 持久保存当前状态,同时还与云中存储的每个用户默认值同步。这可以通过简单地组合多个 Atom 效应来完成(您可以使用 syncEffect() 或其他 Atom 效应来混合匹配)。这些效应按顺序执行,因此最后一个效应具有初始化 Atom 的优先权。

const currentTabState = atom<string>({
key: 'CurrentTab',
default: 'FirstTab', // Fallback default for first-use
effects: [
// Initialize default with per-user default from the cloud
syncEffect({ storeKey: 'user_defaults', refine: string() }),

// Override with state stored in URL if reloading or sharing
syncEffect({ storeKey: 'url', refine: string() }),
],
});

抽象存储

同一个 Atom 也可能根据主机环境与不同的存储同步。例如

const currentUserState = atom<number>({
key: 'CurrentUser',
default: 0,
effects: [
syncEffect({ storeKey: 'ui_state', refine: number() }),
],
});

独立应用程序可能将该 Atom 与 URL 同步

function MyStandaloneApp() {
return (
<RecoilRoot>
<RecoilURLSyncTransit storeKey="ui_state" location={{part: 'hash'}}>
...
</RecoilURLSyncTransit>
</RecoilRoot>
);
}

而另一个使用使用相同 Atom 的组件的应用程序可能希望将其与本地存储同步

function AnotherApp() {
return (
<RecoilRoot>
<RecoilSyncLocalStorage storeKey="ui_state">
...
</RecoilSyncLocalStorage>
</RecoilRoot>
)
}

高级 Atom 映射

Atom 可能不会一对一地映射到外部存储中的项目。 此示例 描述了使用 read 来实现键升级。syncEffect()readwrite 选项可用于实现更复杂的映射。

必须注意高级映射,因为可能存在排序问题,Atom 可能尝试覆盖相同的项目,等等。

多对一

从多个外部项目中提取状态的 Atom 的示例效应

function manyToOneSyncEffect() {
syncEffect({
refine: object({ foo: nullable(number()), bar: nullable(number()) }),
read: ({read}) => ({foo: read('foo'), bar: read('bar')}),
write: ({write, reset}, newValue) => {
if (newValue instanceof DefaultValue) {
reset('foo');
reset('bar');
} else {
write('foo', newValue.foo);
write('bar', newValue.bar);
}
},
});
}

atom<{foo: number, bar: number}>({
key: 'MyObject',
default: {},
effects: [manyToOneSyncEffect()],
});

一对多

从复合外部对象中的道具中提取状态的示例效应

function oneToManySyncEffect(prop: string) {
const validate = assertion(dict(nullable(number())));
syncEffect({
refine: nullable(number()),
read: ({read}) => validate(read('compound'))[prop],
write: ({write, read}, newValue) => {
const compound = {...validate(read('compound'))};
if (newValue instanceof DefaultValue) {
delete compound[prop];
write('compound', compound);
} else {
write('compound', {...compound, [prop]: newValue});
}
},
});
}

atom<number>({
key: 'MyNumber',
default: 0,
effects: [oneToManySyncEffect('foo')],
});