Atom 效果
Atom 效果是一個用於管理副作用及同步或初始化 Recoil 原子的 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 效果比較
Atom 效應大多可以透過 React useEffect()
實作。不過 Atom 集合是在 React 背景之外建立的,而從 React 元件裡面管理效應可能很困難,特別是對於動態建立的 Atom。它們也不能用於初始化 Atom 初始值,也不能與伺服器端渲染一起使用。使用 Atom 效應還會將效應與 Atom 定義放在一起。
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>
);
}
與快照比較
Snapshot hooks
API 也可以監控 Atom 狀態變更,而 <RecoilRoot>
中的 initializeState
prop 可以為初始渲染初始化值。不過,這些 API 監控所有狀態變更,而且處理動態 Atom 可能很棘手,特別是 Atom 家族。有了 Atom 效應,則會在 Atom 定義旁邊分別為每個 Atom 定義副作用,且可以輕鬆地整合多項政策。
記錄範例
一個使用 Atom 效應的簡單範例是記錄特定 Atom 的狀態變更。
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`),
],
});
狀態同步範例
將 Atom 用作特定其他狀態(如遠端資料庫、本機儲存等)的快取值可能很有用。你可以使用 default
屬性與選取器設定 Atom 的預設值,以取得儲存體的值。不過,那只會是一次性查詢;如果儲存體的值變更,Atom 值不會變更。有了效應,我們可以訂閱儲存體並在儲存體變更時更新 Atom 值。從效應呼叫 setSelf()
會將 Atom 初始化為該值,並會用於初始渲染。如果重設 Atom,它會還原為 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),
],
});
寫入快取範例
我們也可以雙向同步 Atom 值與遠端儲存體,讓伺服器上的變更更新 Atom 值,而本機 Atom 中的變更會寫回至伺服器。為了協助避免回饋迴圈,效應在透過該效應的 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
初始化
透過使用 Promise
來同步地呼叫 setSelf()
,你將可以透過 <Suspense/>
元件將 <RecoilRoot/>
中的元件包起來,在等待 Recoil
載入持久化值時顯示備援畫面。<Suspense>
將會顯示備援畫面直到提供給 setSelf()
的 Promise
解決。如果在 Promise
解決之前,原子已經被設定為某個值,那麼初始化的值就會被忽略。
請注意,如果稍後「重置」atoms
,它們將會回復成預設值,而不是初始化值。
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()
}),
],
});
用於持久化值的 key 改變了會怎麼樣?或者以前使用一個 key 持久化的值,現在使用了多個 key 呢?反之亦然?這也可以用類型安全的方式處理
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>
機制在渲染時處理。