React.Suspense とはいったいなんぞや
導入
ReactにReduxを組み込んだ場合「データが取得されるまでローディング画面を表示したい」と思うことは多い。
それを解決するために、storeにローディング用のフラグをはやしたり、containerのstateにフラグをはやしたりと、なんとも面倒な管理をしないといけなくなる。
とはいえ、storeにフラグをはやすのはなんとも不格好だし、containerのstateもダサい、そもそもローディング用のフラグなんて扱いたくない。
そこでふと、「React.Suspenseでうまいこと解決できないものか?」と思うわけです。
結論
無理だった。
実装
すげー適当だけど、こんな感じ。
import React, { FC, Suspense, useCallback, useState } from "react";
import Fuga from "./Fuga";
const Hoge: FC = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, [setCount]);
const [cache, setCache] = useState<string | undefined>();
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<Fuga cache={cache} count={count} setCache={setCache} />
</Suspense>
<button onClick={handleClick}>add</button>
</div>
);
};
export default Hoge;
import React, { FC, useCallback, useMemo } from "react";
export type FugaProps = {
cache?: string;
count: number;
setCache: (cache?: string) => void;
};
const Fuga: FC<FugaProps> = ({ cache, count, setCache }) => {
const getData = useCallback<() => string>(() => {
if (cache === undefined) {
const handleCache = async () => {
const data = await new Promise<string>((resolve) => {
setTimeout(() => {
// resolve(undefined);
resolve("loaded");
}, 1000);
});
setCache(data);
};
throw handleCache();
}
return cache;
}, [cache, setCache]);
const data = useMemo(() => getData(), [getData]);
return (
<div>
<div>{`data: ${data}`}</div>
<div>{`count: ${count}`}</div>
</div>
);
};
export default Fuga;
あんまりデバッグしてないので、間違えてる箇所多いと思います。
イメージということで一つ。
解説
React.Suspenseとは、その子コンポーネントのrender関数が実行されるまでfallbackを表示するコンポーネント、という認識を持ってます。
Fugaコンポーネント内の promise 部分が api の呼び出しに該当する箇所で、reduxだと同期的に呼び出すため、ここが解決できないです。
React.Suspenseのポイントは言わずもがなthrow部分で、throwを返すと関数がもう一度実行されるみたいです、初めて知りました。
例外を再発生させる、のくだりに書かれています。
そのため、先のコードでresolve(undefined)にしてやると、1 秒ごとにずっとgetData内の関数が実行され続け、ずーっとfallbackが表示され続けます。
再度結論
reduxのactionは同期的に呼び出すため、組み込むことは不可能、多分。
やるとしたら、api の呼び出しをcontainer自身で行う必要がある。
余談
React.Suspenseの話をすると、必ず話題に上がるReact.lazyですが、これも非同期にファイルを読み込むからセットみたいな感じなんだなーと、妙に納得しました。