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でキャッシュしてくれるなら、同じクエリを複数回実行しないので、目的は果たせるはずです。
おわりに
地味にめっちゃハマってしまいましたが、ちゃんと理解できて良かったです。
別の解決方法も見つかったのも良かった。
