メインコンテンツにスキップ

アトム効果

アトム効果は、副作用を管理し、Recoilアトムを同期または初期化するためのAPIです。状態の永続化、状態の同期、履歴の管理、ロギングなど、さまざまな便利なアプリケーションがあります。 React effectに似ていますが、アトム定義の一部として定義されているため、各アトムは独自のポリシーを指定および構成できます。また、同期の実装(URL永続化など)やより高度なユースケースについては、recoil-syncライブラリもご覧ください。

アトム効果は、次の定義を持つ関数です。

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... },
],
});

getInfo_UNSTABLE()によって返される情報については、useGetRecoilValueInfo()のドキュメントを参照してください。

React Effectsとの比較

アトム効果は、ほとんどの場合、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>
);
}

スナップショットとの比較

Snapshot hooks 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プロパティを使用してアトムのデフォルト値を設定できます。ただし、これは1回限りのルックアップです。ストアの値が変更されても、アトムの値は変更されません。効果を使用すると、ストアにサブスクライブし、ストアが変更されるたびにアトムの値を更新できます。効果から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による初期化

Promiseを使用してsetSelf()を同期的に呼び出すことにより、<RecoilRoot/>内のコンポーネントを<Suspense/>コンポーネントでラップして、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に初期化する場合とは異なり、アトムのデフォルト値が最初に使用されるため、アトムのデフォルトがPromiseまたは非同期セレクタでない限り、<Suspense>はフォールバックを表示しません。 setSelf()が呼び出される前にアトムが値に設定されている場合、setSelf()によって上書きされます。このアプローチはawaitに限定されるものではなく、setTimeout()など、setSelf()の非同期使用にも使用できます。

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()
}),
],
});

値の永続化に使用されるキーが変更された場合はどうなりますか?または、以前は1つのキーを使用して永続化されていたものが、現在は複数のキーを使用している場合はどうなりますか?またはその逆の場合はどうなりますか?それもタイプセーフな方法で処理できます

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>メカニズムで処理できます。