lamechang-dev

Webフロントエンドエンジニア lamechangのブログ。

【TypeScript】【React】【Recoil】翻訳しながらそれなりに型をつけて細々と進めていくRecoil Tutorial その4 Asynchronous Data Queries編(2)

f:id:lamechang_dev:20220213113950p:plain

本記事は、Reactの提供元であるFacebook改めMetaが開発中の新しい状態管理ライブラリである Recoil の公式チュートリアルを翻訳しつつ、TypeScriptで型付けをしながら進めていく連載の第四弾です。細々とやっていきます🙄 今回は Asynchronous Data Queries の内容について書いていきます。前回が(1)で、今回は(2)に当たります。

recoiljs.org

Concurrent Requests

If you notice in the above example, the friendsInfoQuery uses a query to get the info for each friend. But, by doing this in a loop they are essentially serialized. If the lookup is fast, maybe that's ok. If it's expensive, you can use a concurrency helper such as waitForAll to run them in parallel. This helper accepts both arrays and named objects of dependencies.


上記の例で気付くかもしれませんが、friendsInfoQuery()はQueryを使用して各友達の情報を取得します。ただし、これをループの中で実行することにより、基本的に逐次的に処理されます。探索が速い場合は、おそらくそれで問題ありません。処理に時間がかかる場合は、waitForAllなどの同時実行ヘルパーを使用してそれらを並行して実行できます。このヘルパーは、依存する配列と名前付きオブジェクトの両方を受け入れます。


なるほど、下記のコードの通りに selectorが依存している selectorFamily である useInfoQuery の配列を waitForAll() の引数として渡すことで、内部で実行される非同期処理を逐次処理ではなく並行処理として扱うことができる、ということでしょうね。

json-serverを利用していると非同期の挙動がterminal上での出力することができますが、それを利用すると並列処理が走り効率的に非同期を処理していることがわかりやすいです。

f:id:lamechang_dev:20220223162257g:plain

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


You can use waitForNone to handle incremental updates to the UI with partial data


waitForNoneを使用して、部分的なデータを使用したUIの増分更新を処理できます

waitForNone は渡された依存関係の状態の Loadable オブジェクトを返します。 例えば今回の例のように依存関係にある selectorFamily userInfoQuery の配列について、並列的にそれぞれの非同期を実行しPromiseが解決したものから部分的にUIを更新する、といったような挙動を以下のようなコードで実現することができるみたいです。

ちなみに、Loadable の詳細はこちらLoadable オブジェクトはatom もしくは selectorの現在の状態を表現しており使用可能な値があるか、エラー状態にあるか、非同期解決が保留されている可能性があります。これらはそれぞれhasValuehasError、または loadingで表現されます。

以下の例の friendsInfoQueryreturn部分を見ると、取得した friendsのうち statehasValueであるもののみにfilterをしていることがわかります。

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

Pre-Fetching

For performance reasons you may wish to kick off fetching before rendering. That way the query can be going while we start rendering. The React docs give some examples. This pattern works with Recoil as well.


パフォーマンス上の理由から、レンダリングの前にフェッチを開始したいと考えるかもしれません。そうすれば、レンダリングを開始するときにクエリを実行できます。 Reactのドキュメントにはいくつかの例があります。このパターンはリコイルでも機能します。


Let's change the above example to initiate a fetch for the next user info as soon as the user clicks the button to change users:


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

これ、突然 useRecoilCallback が登場してきてるので、そちらの挙動の確認をした方がいいでしょう。

さてuseRecoilCallbackですが、こちらは subscribeさせずに atomもしくは selector を読み込んで何かをする、という時に利用されると考えます。subscribeによる状態管理は、値の変更を常に応じて状態を常に更新する必要が出てきてしまいますが、下記の例のように onClickが走った時のみに値を取得し、再レンダリングを行いたい、といったことを実現することができます。

const CurrentUserInfo: React.VFC = () => {
  const currentUser = useRecoilValue(currentUserInfoQuery);
  const friends = useRecoilValue(friendsInfoQuery);

  const changeUser = useRecoilCallback<Array<number>, void>(({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>
  )};

export default CurrentUserInfo;


Note that this pre-fetching works by triggering the selectorFamily() to initiate an async query and populate the selector's cache. If you are using an atomFamily() instead, by either setting the atoms or relying on atom effects to initialize, then you should use useRecoilTransaction_UNSTABLE() instead of useRecoilCallback(), as trying to set the state of the provided Snapshot will have no effect on the live state in the host .


このプリフェッチは、selectorfamily()による非同期クエリの実行と、selectorのキャッシュを入することによって機能します。代わりにatomFamily()を使用している場合は、Atomを設定したり、atoms effectsによる初期化を利用することで、userecoilCallback()の代わりにuseRecoilTransaction_UNSTABLE()を使用してください。提供されたSnapshotのstateを設定しようとしても、RecoilRootのstateには影響しません。


Asynchronous Data Queries の部分は新しい情報がモリモリですね。Query Default Atom Values以降については更に記事を分けて、(3)としたいと思います。