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

非同期データクエリ

Recoilは、データフローグラフを介して状態と派生状態をReactコンポーネントにマッピングする方法を提供します。非常に強力なのは、グラフ内の関数が非同期にもなり得ることです。これにより、同期Reactコンポーネントのレンダー関数で非同期関数を簡単に使用できます。 Recoilを使用すると、セレクターのデータフローグラフで同期関数と非同期関数をシームレスに混在させることができます。セレクターの`get`コールバックから値自体ではなく、値へのPromiseを返すだけで、インターフェースはまったく同じままです。これらは単なるセレクターであるため、他のセレクターもそれらに依存してデータをさらに変換できます。

セレクターは、非同期データをRecoilデータフローグラフに組み込む1つの方法として使用できます。セレクターは「冪等」関数を表すことに注意してください。特定の入力セットに対して、常に同じ結果を生成する必要があります(少なくともアプリケーションの存続期間中)。これは、セレクターの評価がキャッシュされる、再起動される、または複数回実行される可能性があるため重要です。このため、セレクターは一般に、読み取り専用DBクエリをモデル化するのに適した方法です。可変データの場合は、クエリのリフレッシュを使用できます。または、可変状態を同期したり、状態を永続化したり、その他の副作用が発生したりする場合は、アトム効果 APIまたはRecoil Syncライブラリを検討してください。

同期例

たとえば、ユーザー名を取得するための単純な同期アトムセレクターを次に示します。

const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
});

const currentUserNameState = selector({
key: 'CurrentUserName',
get: ({get}) => {
return tableOfUsers[get(currentUserIDState)].name;
},
});

function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameState);
return <div>{userName}</div>;
}

function MyApp() {
return (
<RecoilRoot>
<CurrentUserInfo />
</RecoilRoot>
);
}

非同期例

ユーザー名がクエリする必要があるデータベースに格納されている場合、`Promise`を返すか、`async`関数を使用するだけです。依存関係が変更されると、セレクターが再評価され、新しいクエリが実行されます。結果はキャッシュされるため、クエリは一意の入力ごとに1回だけ実行されます。

const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});

function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}

セレクターのインターフェースは同じであるため、このセレクターを使用するコンポーネントは、同期アトム状態、派生セレクター状態、または非同期クエリのいずれで裏付けられているかを気にする必要はありません。

ただし、Reactレンダー関数は同期であるため、promiseが解決される前に何がレンダリングされるのでしょうか? Recoilは、React Suspenseと連携して、保留中のデータを処理するように設計されています。コンポーネントをSuspense境界でラップすると、まだ保留中のすべての子孫がキャッチされ、フォールバックUIがレンダリングされます。

function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}

エラー処理

しかし、リクエストにエラーがあった場合はどうでしょうか? Recoilセレクターはエラーをスローすることもでき、コンポーネントがその値を使用しようとするとスローされます。これは、React `<ErrorBoundary>`でキャッチできます。例えば

const currentUserNameQuery = selector({
key: 'CurrentUserName',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
if (response.error) {
throw response.error;
}
return response.name;
},
});

function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}

function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}

パラメーター付きクエリ

派生状態に基づくだけではないパラメーターに基づいてクエリを実行できるようにしたい場合があります。たとえば、コンポーネントの小道具に基づいてクエリを実行したい場合があります。`selectorFamily()`ヘルパーを使用してこれを行うことができます。

const userNameQuery = selectorFamily({
key: 'UserName',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.name;
},
});

function UserInfo({userID}) {
const userName = useRecoilValue(userNameQuery(userID));
return <div>{userName}</div>;
}

function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<UserInfo userID={1}/>
<UserInfo userID={2}/>
<UserInfo userID={3}/>
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}

データフローグラフ

クエリをセレクターとしてモデル化することで、状態、派生状態、およびクエリを混在させたデータフローグラフを構築できることを忘れないでください。このグラフは、状態が更新されると自動的に更新され、Reactコンポーネントを再レンダリングします。

次の例では、現在のユーザーの名前とその友人のリストが表示されます.友人の名前をクリックすると、その友人が現在のユーザーになり、名前とリストが自動的に更新されます.

const currentUserIDState = atom({
key: 'CurrentUserID',
default: null,
});

const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response;
},
});

const currentUserInfoQuery = selector({
key: 'CurrentUserInfoQuery',
get: ({get}) => get(userInfoQuery(get(currentUserIDState))),
});

const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
return friendList.map(friendID => get(userInfoQuery(friendID)));
},
});

function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);
const setCurrentUserID = useSetRecoilState(currentUserIDState);
return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map(friend =>
<li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
{friend.name}
</li>
)}
</ul>
</div>
);
}

function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}

同時リクエスト

上記の例で気づいた場合、`friendsInfoQuery`はクエリを使用して各友人の情報を取得します。ただし、ループでこれを行うことにより、それらは本質的にシリアル化されます。ルックアップが高速であれば、問題ないかもしれません。費用がかかる場合は、`waitForAll`などの同時実行ヘルパーを使用して、並列で実行できます。このヘルパーは、依存関係の配列と名前付きオブジェクトの両方を受け入れます。

const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friends = get(waitForAll(
friendList.map(friendID => userInfoQuery(friendID))
));
return friends;
},
});

部分的なデータでUIの増分更新を処理するには、`waitForNone`を使用できます。

const friendsInfoQuery = selector({
key: 'FriendsInfoQuery',
get: ({get}) => {
const {friendList} = get(currentUserInfoQuery);
const friendLoadables = get(waitForNone(
friendList.map(friendID => userInfoQuery(friendID))
));
return friendLoadables
.filter(({state}) => state === 'hasValue')
.map(({contents}) => contents);
},
});

プリフェッチ

パフォーマンス上の理由から、レンダリングの*前*にフェッチを開始することをお勧めします。そうすれば、レンダリングの開始中にクエリを実行できます。Reactのドキュメントにいくつかの例が示されています。このパターンはRecoilでも機能します。

上記の例を変更して、ユーザーがユーザーを変更するためのボタンをクリックするとすぐに、次のユーザー情報のフェッチを開始しましょう。

function CurrentUserInfo() {
const currentUser = useRecoilValue(currentUserInfoQuery);
const friends = useRecoilValue(friendsInfoQuery);

const changeUser = useRecoilCallback(({snapshot, set}) => userID => {
snapshot.getLoadable(userInfoQuery(userID)); // pre-fetch user info
set(currentUserIDState, userID); // change current user to start new render
});

return (
<div>
<h1>{currentUser.name}</h1>
<ul>
{friends.map(friend =>
<li key={friend.id} onClick={() => changeUser(friend.id)}>
{friend.name}
</li>
)}
</ul>
</div>
);
}

このプリフェッチは、`selectorFamily()`をトリガーして非同期クエリを開始し、セレクターのキャッシュを設定することによって機能することに注意してください。`atomFamily()`を使用している場合、アトムを設定するか、アトムエフェクトに依存して初期化することにより、ホスト`<RecoilRoot>`のライブ状態には影響しないため、`useRecoilCallback()`の代わりに`useRecoilTransaction_UNSTABLE()`を使用する必要があります。提供された`Snapshot`の状態を設定しようとしても、。

クエリのデフォルトアトム値

一般的なパターンは、アトムを使用してローカルの編集可能な状態を表しますが、promiseを使用してデフォルト値をクエリすることです。

const currentUserIDState = atom({
key: 'CurrentUserID',
default: myFetchCurrentUserID(),
});

または、セレクターを使用してクエリを延期するか、他の状態に依存します。セレクターを使用する場合、デフォルトのアトム値は動的なままであり、ユーザーがアトムを明示的に設定するまで、セレクターの更新とともに更新されることに注意してください。

const UserInfoState = atom({
key: 'UserInfo',
default: selector({
key: 'UserInfo/Default',
get: ({get}) => myFetchUserInfo(get(currentUserIDState)),
}),
});

これはアトムファミリでも使用できます。

const userInfoState = atomFamily({
key: 'UserInfo',
default: id => myFetchUserInfo(id),
});
const userInfoState = atomFamily({
key: 'UserInfo',
default: selectorFamily({
key: 'UserInfo/Default',
get: id => ({get}) => myFetchUserInfo(id, get(paramsState)),
}),
});

データの双方向同期が必要な場合は、アトム効果を検討してください。

React Suspenseなしの非同期クエリ

保留中の非同期セレクターを処理するためにReact Suspenseを使用する必要はありません。`useRecoilValueLoadable()`フックを使用して、レンダリング中の現在の状態を判断することもできます。

function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
throw userNameLoadable.contents;
}
}

クエリのリフレッシュ

セレクターを使用してデータクエリをモデル化する場合、セレクターの評価は、特定の状態に対して常に一貫した値を提供する必要があります。セレクターは、他のアトムおよびセレクターの状態から派生した状態を表します.したがって、セレクターの評価関数は、キャッシュされるか複数回実行される可能性があるため、特定の入力に対して冪等である必要があります。ただし、セレクターがデータクエリからデータを取得する場合、新しいデータでリフレッシュしたり、障害後に再試行したりするために、再クエリすることが役立つ場合があります。これを達成するには、いくつかの方法があります.

`useRecoilRefresher()`

`useRecoilRefresher_UNSTABLE()`フックを使用して、キャッシュをクリアして再評価を強制するために呼び出すことができるコールバックを取得できます。

const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async () => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.data;
}
})

function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRecoilRefresher_UNSTABLE(userInfoQuery(currentUserID));

return (
<div>
<h1>{currentUserInfo.name}</h1>
<button onClick={() => refreshUserInfo()}>Refresh</button>
</div>
);
}

リクエストIDの使用

セレクターの評価は、入力(依存状態またはファミリパラメータ)に基づいて、特定の状態に対して一貫した値を提供する必要があります。したがって、リクエストIDをファミリパラメータまたはクエリの依存関係として追加できます。例えば

const userInfoQueryRequestIDState = atomFamily({
key: 'UserInfoQueryRequestID',
default: 0,
});

const userInfoQuery = selectorFamily({
key: 'UserInfoQuery',
get: userID => async ({get}) => {
get(userInfoQueryRequestIDState(userID)); // Add request ID as a dependency
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
}
return response.data;
},
});

function useRefreshUserInfo(userID) {
const setUserInfoQueryRequestID = useSetRecoilState(userInfoQueryRequestIDState(userID));
return () => {
setUserInfoQueryRequestID(requestID => requestID + 1);
};
}

function CurrentUserInfo() {
const currentUserID = useRecoilValue(currentUserIDState);
const currentUserInfo = useRecoilValue(userInfoQuery(currentUserID));
const refreshUserInfo = useRefreshUserInfo(currentUserID);

return (
<div>
<h1>{currentUserInfo.name}</h1>
<button onClick={refreshUserInfo}>Refresh</button>
</div>
);
}

アトムの使用

別のオプションは、セレクターの代わりにアトムを使用して、クエリ結果をモデル化することです。更新ポリシーに基づいて、新しいクエリ結果でアトムの状態を命令的に更新できます。

const userInfoState = atomFamily({
key: 'UserInfo',
default: userID => fetch(userInfoURL(userID)),
});

// React component to refresh query
function RefreshUserInfo({userID}) {
const refreshUserInfo = useRecoilCallback(({set}) => async id => {
const userInfo = await myDBQuery({userID});
set(userInfoState(userID), userInfo);
}, [userID]);

// Refresh user info every second
useEffect(() => {
const intervalID = setInterval(refreshUserInfo, 1000);
return () => clearInterval(intervalID);
}, [refreshUserInfo]);

return null;
}

アトムは*現在*、新しい値として`Promise`を受け入れることをサポートしていないことに注意してください。したがって、目的の動作である場合、クエリのリフレッシュが保留中の間、React Suspenseの保留状態にアトムを配置することはできません。ただし、現在の読み込み状態と実際の結果を手動でエンコードするオブジェクトを格納して、これを明示的に処理できます。

アトムのクエリ同期については、アトム効果も検討してください。

エラーメッセージからのクエリの再試行

`<ErrorBoundary>`でスローおよびキャッチされたエラーに基づいてクエリを見つけて再試行する、ちょっとした楽しい例を次に示します。

function QueryErrorMessage({error}) {
const snapshot = useRecoilSnapshot();
const selectors = useMemo(() => {
const ret = [];
for (const node of snapshot.getNodes_UNSTABLE({isInitialized: true})) {
const {loadable, type} = snapshot.getInfo_UNSTABLE(node);
if (loadable != null && loadable.state === 'hasError' && loadable.contents === error) {
ret.push(node);
}
}
return ret;
}, [snapshot, error]);
const retry = useRecoilCallback(({refresh}) =>
() => selectors.forEach(refresh),
[selectors],
);

return selectors.length > 0 && (
<div>
Error: {error.toString()}
Query: {selectors[0].key}
<button onClick={retry}>Retry</button>
</div>
);
}