経験知ロゴ

useRecoilRefresher_UNSTABLEが非同期処理だとうまく再読込してくれないときの対処法

refresher

ReactのRecoilでSelecterをリロードするには「useRecoilRefresher_UNSTABLE」を使います。「UNSTABLE」が付いているので、まだ動作が不安定な可能性があり、名前も変わる可能性があります。

Selecterに非同期処理を書いていたのですが、useRecoilRefresher_UNSTABLEを使ってリロードすると下記のようなエラーが出ます。

Error: A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition.

Selecterを同期的に書くとリロードはうまく読み込んでくれるので、非同期処理だとうまくいかないようです。

useRecoilValueで読み込んだ値を使っているコンポーネントを、Suspenseコンポーネントで囲ってあげたら、再読込してくれました。

ダメだった例

// bad
const myQuery = selector({
  key: 'MyQuery',
  get: () => fetch(myQueryURL),
});

function MyComponent() {
  const data = useRecoilValue(myQuery);
  const refresh = useRecoilRefresher_UNSTABLE(myQuery);

  return (
    <div>
      Data: {data}
      <button onClick={() => refresh()}>Refresh</button>
    </div>
  );
}

function App() {
  return (<MyComponent />) 
  // selecter再読み込み時の処理がないので、最初は表示されるが再読込するとエラー
}

うまく行った例。

import React, { Suspense } from "react";

const myQuery = selector({
  key: 'MyQuery',
  get: () => fetch(myQueryURL),
});

function MyComponent() {
  const data = useRecoilValue(myQuery);
  const refresh = useRecoilRefresher_UNSTABLE(myQuery);

  return (
    <div>
      Data: {data}
      <button onClick={() => refresh()}>Refresh</button>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>loading</div>}>
      <MyComponent />
    </Suspense>
  ) 
  // Suspenseで囲まれているため、MyComponentがPromiseを返すとSuspenseがfallbackを表示してくれる
}

useRecoilRefresher_UNSTABLEを使ってリフレッシュすると、useRecoilValueの返り値の値がPromiseを返し、MyComponent自体がPromiseの状態になるのに、その時の処理ができていないのがエラーの原因でした。

Suspenseで囲ってあげれば、MyComponentが「表示できない!」と言ってきても、Suspenseが処理してくれるので問題なく表示できるんですね。

最初、MyComponent内のreternにSuspenseを使っていたのですが、MyComponenコンポーネント自体がローディング状態を返すので、コンポーネント内ではなく、コンポーネントを読み込んでいる箇所をSuspenseで囲わなければダメなのに気づくのが遅れました。

他にも、「useRecoilValueLoadable」を使ってローディング状態の処理を書いておけばコンポーネント内で解決できますが、やや助長になります。

function MyComponent() {
  const data = useRecoilValueLoadable(myQuery);
  const refresh = useRecoilRefresher_UNSTABLE(myQuery);
  
  if ( data.state === 'loading' ) {
    return (<div>loading</div>)
  }
  
  if ( data.state === 'hasValue' ) {
    return (
      <div>
        Data: {data}
        <button onClick={() => refresh()}>Refresh</button>
      </div>
    );
  }
  
  if ( data.state === 'hasError' ) {
    // エラー処理
  }
}

再読み込み時にチラつく問題

一応の動作はできたのですが、「useRecoilRefresher_UNSTABLE」を使って再読込すると、読み込みが開始した瞬間にSuspenseのfallbackが読み込まれるので、どんなに速い非同期処理をしても一瞬チラつきます。

再読込するときはそのまま表示しつつ、非同期処理が終わったら新しい表示をしてもらいたいのですが、現状だとできなさそうです。

React Queryならキャッシュを使った再読み込みが出来るようなので、そちらを使ってみようかなと。

Recoilで非同期処理を使いたいのは、非同期処理の結果を複数のコンポーネントで使いまわしたいからなので、React Queryでキャッシュしてくれるなら、同じクエリを複数回実行しないので、目的は果たせるはずです。

おわりに

地味にめっちゃハマってしまいましたが、ちゃんと理解できて良かったです。

別の解決方法も見つかったのも良かった。

役に立ったらこの記事のシェアをお願いします

ブログのフォロー・RSS購読は下記ボタンから