同步原子效應 - syncEffect()
syncEffect()
是一種 原子效應,用於標記應該同步的原子並讓它們使用外部儲存來初始化值。唯一需要的選項是 refine
,用於輸入驗證。itemKey
選項可讓您針對外部儲存指定此特定原子的金鑰。如果未指定,它會預設為原子的金鑰本身。也可以提供 storeKey
以比對要同步到的外部儲存(如果您有多個儲存)。還有更多進階案例的選項,例如 read
和 write
。
輸入驗證
為驗證來自外部系統的輸入,並將雜湊式輸入 refine 為強類型 Flow 或 TypeScript 輸入,recoil-sync
使用 Refine 函式庫。此函式庫使用一套可組合的函數來描述類型並執行執行時期驗證。 syncEffect()
的 refine
屬性取用 Refine Checker
。Refine 檢查器的類型必須與原子的類型相符。
單純字串原子的範例效應
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(),
}),
],
});
原子群組
原子群組 中的原子也可以使用 syncEffect()
進行同步。群組中的每個原子都被視為一個要同步的單一項目。預設項目金鑰會包含群組參數的序列化資訊。如果您指定自己的 itemKey
,則也應該編碼群組參數以唯一標識每個原子;可以使用原子群組 effects
選項的回呼函數取得參數。
atomFamily({
key: 'AtomKey',
effects: param => [
syncEffect({
itemKey: `myItem-${param}`,
storeKey: 'storeA',
refine: string(),
}),
],
});
向下相容性
支援舊版系統或具有先前狀態版本的外部系統很重要。有幾個機制可以做到這點
升級原子類型
如果一個原子持續保存到儲存體,而您從那時起就變更了原子的類型,您可以使用 Refine 的 match()
和 asType()
來升級類型。這個範例會讀取目前為數字的 ID,但先前儲存為字串或物件。它會升級先前的類型,而且原子會始終儲存最新類型。
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)),
}),
],
});
升級原子金鑰
原子的金鑰可能會隨著時間改變。read
選項允許我們指定如何從外部儲存體讀取原子
const myAtom = atom<number>({
key: 'MyAtom',
default: 0,
effects: [
syncEffect({
itemKey: 'new_key',
read: ({read}) => read('new_key') ?? read('old_key'),
}),
],
});
讀取時可能有更複雜的轉換,請見下文。
升級原子儲存
您也可以使用多個效果將原子轉移到新的外部儲存體中進行同步。
const myAtom = atom<number>({
key: 'MyAtom',
default: 0,
effects: [
syncEffect({ storeKey: 'old_store', refine: number() }),
syncEffect({ storeKey: 'new_store', refine: number() }),
],
});
與多個儲存體同步
一個原子可能需要時常與多個儲存系統同步。例如,某個 UI 狀態的原子可能會想持續儲存當前狀態給可分享的 URL,同時也與雲端中儲存的每個使用者預設值同步。這只要透過撰寫多個原子效果即可輕鬆辦到 (你可以使用 syncEffect()
或其他原子效果混搭)。效果會按順序執行,因此最後一個效果優先初始化原子。
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() }),
],
});
抽象儲存
同一個原子也可能依據主機環境與不同的儲存空間同步。例如:
const currentUserState = atom<number>({
key: 'CurrentUser',
default: 0,
effects: [
syncEffect({ storeKey: 'ui_state', refine: number() }),
],
});
獨立應用程式可能會將該原子與 URL 同步。
function MyStandaloneApp() {
return (
<RecoilRoot>
<RecoilURLSyncTransit storeKey="ui_state" location={{part: 'hash'}}>
...
</RecoilURLSyncTransit>
</RecoilRoot>
);
}
而使用與該原子相同的元件的其他應用程式可能會想將該原子與當地儲存空間同步。
function AnotherApp() {
return (
<RecoilRoot>
<RecoilSyncLocalStorage storeKey="ui_state">
...
</RecoilSyncLocalStorage>
</RecoilRoot>
)
}
進階原子對應
原子可能無法一對一對應外部儲存中的項目。 這個範例 說明如何使用 read
來實作金鑰升級。 syncEffect()
的 read
及 write
選項可供實作更複雜的對應。
對進階對應必須小心處理,因為可能會發生排序問題,原子可能會試圖覆寫同一個項目等問題。
多對一
取用狀態從多個外部項目拉取的原子的效果範例
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()],
});
一對多
從複合外部物件中的 prop 拉取狀態的效果範例
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')],
});