useRecoilRefresher_UNSTABLEが非同期処理だとうまく再読込してくれないときの対処法
※本ページはプロモーションが含まれています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でキャッシュしてくれるなら、同じクエリを複数回実行しないので、目的は果たせるはずです。
おわりに
地味にめっちゃハマってしまいましたが、ちゃんと理解できて良かったです。
別の解決方法も見つかったのも良かった。