同步 Atom 效应 - syncEffect()
syncEffect()
是一个 Atom 效应,用于标记应同步的 Atom,并使用外部存储初始化其值。唯一必需的选项是用于输入验证的 refine
。itemKey
选项允许您为外部存储中的此特定 Atom 指定一个键。如果未指定,它默认为 Atom 自己的键。还可以提供 storeKey
以匹配要同步的外部存储,如果您有多个存储的话。还有其他选项,例如用于更高级情况的 read
和 write
。
输入验证
为了验证来自外部系统的输入并从 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()
的 read
和 write
选项可用于实现更复杂的映射。
必须注意高级映射,因为可能存在排序问题,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')],
});