原子效应
原子效应是用于管理副作用以及同步或初始化回弹原子的 API。它们有多种有用的应用,例如状态持久性、状态同步、管理历史记录、日志记录等等。它们类似于 React 效应,但作为原子定义的一部分定义,因此每个原子都可以指定和组合自己的策略。还可以查看 recoil-sync
库以获取有关同步的某些实现(例如 URL 持久性)或更高级的用例。
原子效应 是一个具有以下定义的 函数。
type AtomEffect<T> = ({
node: RecoilState<T>, // A reference to the atom itself
storeID: StoreID, // ID for the <RecoilRoot> or Snapshot store associated with this effect.
// ID for the parent Store the current instance was cloned from. For example,
// the host <RecoilRoot> store for `useRecoilCallback()` snapshots.
parentStoreID_UNSTABLE: StoreID,
trigger: 'get' | 'set', // The action which triggered initialization of the atom
// Callbacks to set or reset the value of the atom.
// This can be called from the atom effect function directly to initialize the
// initial value of the atom, or asynchronously called later to change it.
setSelf: (
| T
| DefaultValue
| Promise<T | DefaultValue> // Only allowed for initialization at this time
| WrappedValue<T>
| ((T | DefaultValue) => T | DefaultValue),
) => void,
resetSelf: () => void,
// Subscribe to changes in the atom value.
// The callback is not called due to changes from this effect's own setSelf().
onSet: (
(newValue: T, oldValue: T | DefaultValue, isReset: boolean) => void,
) => void,
// Callbacks to read other atoms/selectors
getPromise: <S>(RecoilValue<S>) => Promise<S>,
getLoadable: <S>(RecoilValue<S>) => Loadable<S>,
getInfo_UNSTABLE: <S>(RecoilValue<S>) => RecoilValueInfo<S>,
}) => void | () => void; // Optionally return a cleanup handler
原子效应通过 effects
选项附加到 原子。每个原子都可以引用这些原子效应函数的数组,这些函数在原子初始化时按优先级顺序调用。当原子在 <RecoilRoot>
中首次使用时,会初始化原子,但如果它们未被使用且已清除,则可能会再次重新初始化。原子效应函数可以返回一个可选的清除处理程序来管理清除副作用。
const myState = atom({
key: 'MyKey',
default: null,
effects: [
() => {
...effect 1...
return () => ...cleanup effect 1...;
},
() => { ...effect 2... },
],
});
原子族 支持参数化或非参数化效应
const myStateFamily = atomFamily({
key: 'MyKey',
default: null,
effects: param => [
() => {
...effect 1 using param...
return () => ...cleanup effect 1...;
},
() => { ...effect 2 using param... },
],
});
请参阅 useGetRecoilValueInfo()
以获取有关 getInfo_UNSTABLE()
返回的信息的文档。
与 React 效应相比
原子效应大部分可以通过 React useEffect()
实现。但是,原子集是在 React 上下文之外创建的,在 React 组件内管理效应可能很困难,特别是对于动态创建的原子。它们也不能用于初始化初始原子值或与服务器端渲染一起使用。使用原子效应还将效应与原子定义共置。
const myState = atom({key: 'Key', default: null});
function MyStateEffect(): React.Node {
const [value, setValue] = useRecoilState(myState);
useEffect(() => {
// Called when the atom value changes
store.set(value);
store.onChange(setValue);
return () => { store.onChange(null); }; // Cleanup effect
}, [value]);
return null;
}
function MyApp(): React.Node {
return (
<div>
<MyStateEffect />
...
</div>
);
}
与快照相比
快照钩子
API 也可以监控原子状态更改,并且 <RecoilRoot>
中的 initializeState
属性可以初始化初始渲染的值。但是,这些 API 监控所有状态更改,并且管理动态原子(特别是原子族)可能会很麻烦。使用原子效应,可以根据原子定义为每个原子定义副作用,并且可以轻松地组合多个策略。
日志记录示例
使用原子效应的一个简单示例是记录特定原子的状态更改。
const currentUserIDState = atom({
key: 'CurrentUserID',
default: null,
effects: [
({onSet}) => {
onSet(newID => {
console.debug("Current user ID:", newID);
});
},
],
});
历史记录示例
日志记录的更复杂示例可能是维护更改历史记录。此示例提供了一个效应,该效应维护一个包含状态更改历史记录的队列,以及撤消该特定更改的回调处理程序
const history: Array<{
label: string,
undo: () => void,
}> = [];
const historyEffect = name => ({setSelf, onSet}) => {
onSet((newValue, oldValue) => {
history.push({
label: `${name}: ${JSON.serialize(oldValue)} -> ${JSON.serialize(newValue)}`,
undo: () => {
setSelf(oldValue);
},
});
});
};
const userInfoState = atomFamily({
key: 'UserInfo',
default: null,
effects: userID => [
historyEffect(`${userID} user info`),
],
});
状态同步示例
将原子用作其他状态(例如远程数据库、本地存储等等)的本地缓存值可能很有用。可以使用 default
属性和选择器来设置原子的默认值以获取存储的值。但是,这只是一个一次性查找;如果存储的值发生更改,原子值将不会发生更改。使用效应,我们可以订阅存储并在存储发生更改时更新原子的值。从效应中调用 setSelf()
将初始化原子为该值,并将用于初始渲染。如果原子被重置,它将恢复为 default
值,而不是初始化的值。
const syncStorageEffect = userID => ({setSelf, trigger}) => {
// Initialize atom value to the remote storage state
if (trigger === 'get') { // Avoid expensive initialization
setSelf(myRemoteStorage.get(userID)); // Call synchronously to initialize
}
// Subscribe to remote storage changes and update the atom value
myRemoteStorage.onChange(userID, userInfo => {
setSelf(userInfo); // Call asynchronously to change value
});
// Cleanup remote storage subscription
return () => {
myRemoteStorage.onChange(userID, null);
};
};
const userInfoState = atomFamily({
key: 'UserInfo',
default: null,
effects: userID => [
historyEffect(`${userID} user info`),
syncStorageEffect(userID),
],
});
写入缓存示例
我们还可以双向同步原子值与远程存储,以便服务器上的更改更新原子值,而本地原子中的更改被写回服务器。当通过该效应的 setSelf()
更改时,该效应不会调用 onSet()
处理程序,以帮助避免反馈循环。
const syncStorageEffect = userID => ({setSelf, onSet, trigger}) => {
// Initialize atom value to the remote storage state
if (trigger === 'get') { // Avoid expensive initialization
setSelf(myRemoteStorage.get(userID)); // Call synchronously to initialize
}
// Subscribe to remote storage changes and update the atom value
myRemoteStorage.onChange(userID, userInfo => {
setSelf(userInfo); // Call asynchronously to change value
});
// Subscribe to local changes and update the server value
onSet(userInfo => {
myRemoteStorage.set(userID, userInfo);
});
// Cleanup remote storage subscription
return () => {
myRemoteStorage.onChange(userID, null);
};
};
本地存储持久性
原子效应可用于使用 浏览器本地存储 持久化原子状态。localStorage
是同步的,因此我们可以直接检索数据,而无需使用 async
await
或 Promise
。
请注意,以下示例为了说明目的而进行了简化,并且没有涵盖所有情况。
const localStorageEffect = key => ({setSelf, onSet}) => {
const savedValue = localStorage.getItem(key)
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
onSet((newValue, _, isReset) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
effects: [
localStorageEffect('current_user'),
]
});
异步存储
如果持久化数据需要异步检索,则可以 在 setSelf()
函数中使用 Promise
或 异步调用它。
下面我们将使用 AsyncLocalStorage
或 localForage
作为异步存储的示例。
使用 Promise
初始化
通过同步调用 setSelf()
并传递一个 Promise
,你将能够使用 <Suspense/>
组件包装 <RecoilRoot/>
中的组件,以便在等待 Recoil
加载持久化值时显示回退。<Suspense>
将显示回退,直到传递给 setSelf()
的 Promise
解析。如果在 Promise
解析之前将原子设置为某个值,则会忽略初始化的值。
请注意,如果 原子
后来被“重置”,它们将恢复为其默认值,而不是初始化的值。
const localForageEffect = key => ({setSelf, onSet}) => {
setSelf(localForage.getItem(key).then(savedValue =>
savedValue != null
? JSON.parse(savedValue)
: new DefaultValue() // Abort initialization if no value was stored
));
// Subscribe to state changes and persist them to localForage
onSet((newValue, _, isReset) => {
isReset
? localForage.removeItem(key)
: localForage.setItem(key, JSON.stringify(newValue));
});
};
const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
effects: [
localForageEffect('current_user'),
]
});
异步 setSelf()
使用这种方法,你可以在值可用时异步调用 setSelf()
。与初始化为 Promise
不同,最初将使用原子的默认值,因此 <Suspense>
不会显示回退,除非原子的默认值是 Promise
或异步选择器。如果在调用 setSelf()
之前将原子设置为某个值,则它将被 setSelf()
覆盖。这种方法不仅限于 await
,还适用于 setSelf()
的任何异步使用,例如 setTimeout()
。
const localForageEffect = key => ({setSelf, onSet, trigger}) => {
// If there's a persisted value - set it on load
const loadPersisted = async () => {
const savedValue = await localForage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
};
// Asynchronously set the persisted data
if (trigger === 'get') {
loadPersisted();
}
// Subscribe to state changes and persist them to localForage
onSet((newValue, _, isReset) => {
isReset
? localForage.removeItem(key)
: localForage.setItem(key, JSON.stringify(newValue));
});
};
const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
effects: [
localForageEffect('current_user'),
]
});
向后兼容性
如果你更改了原子的格式怎么办?使用基于旧格式的 localStorage
加载具有新格式的页面可能会导致问题。你可以构建效应来以类型安全的方式处理恢复和验证值
type PersistenceOptions<T>: {
key: string,
validate: mixed => T | DefaultValue,
};
const localStorageEffect = <T>(options: PersistenceOptions<T>) => ({setSelf, onSet}) => {
const savedValue = localStorage.getItem(options.key)
if (savedValue != null) {
setSelf(options.validate(JSON.parse(savedValue)));
}
onSet(newValue => {
localStorage.setItem(options.key, JSON.stringify(newValue));
});
};
const currentUserIDState = atom<number>({
key: 'CurrentUserID',
default: 1,
effects: [
localStorageEffect({
key: 'current_user',
validate: value =>
// values are currently persisted as numbers
typeof value === 'number'
? value
// if value was previously persisted as a string, parse it to a number
: typeof value === 'string'
? parseInt(value, 10)
// if type of value is not recognized, then use the atom's default value.
: new DefaultValue()
}),
],
});
如果用于持久化值的键发生更改怎么办?或者过去使用一个键持久化的内容现在使用多个键?或者反之亦然?这也可以以类型安全的方式处理
type PersistenceOptions<T>: {
key: string,
validate: (mixed, Map<string, mixed>) => T | DefaultValue,
};
const localStorageEffect = <T>(options: PersistenceOptions<T>) => ({setSelf, onSet}) => {
const savedValues = parseValuesFromStorage(localStorage);
const savedValue = savedValues.get(options.key);
setSelf(
options.validate(savedValue ?? new DefaultValue(), savedValues),
);
onSet((newValue, _, isReset) => {
isReset
? localStorage.removeItem(key)
: localStorage.setItem(key, JSON.stringify(newValue));
});
};
const currentUserIDState = atom<number>({
key: 'CurrentUserID',
default: 1,
effects: [
localStorageEffect({
key: 'current_user',
validate: (value, values) => {
if (typeof value === 'number') {
return value;
}
const oldValue = values.get('old_key');
if (typeof oldValue === 'number') {
return oldValue;
}
return new DefaultValue();
},
}),
],
});
错误处理
如果在原子效应执行过程中抛出错误,则原子将使用该错误初始化为错误状态。然后,这可以通过渲染时的标准 React <ErrorBoundary>
机制进行处理。