概要
Reactを使っていると、PromiseではなくSuspenseにしたいときが度々ありますが、都度変換のコードを書くのが面倒になったので汎用的な変換関数を作りました。
コード
import { useEffect, useRef } from "react";
/**
* キーを定義したい場合、こちらに追記してください
* 定義したキーのみ受け付けたい場合、 |から後ろを消してください
*/
type Key = 'test' | (string & {});
const result = new Map<Key, {
timeoutId: number;
value: unknown;
}>();
const waiting = new Map<Key, Promise<unknown>>;
type PromiseResult<FnType extends (...args: unknown[]) => Promise<unknown>> = Awaited<
Promise<ReturnType<FnType>>
>;
/**
* コンポーネントが破棄されたとき、cacheTime以内に同一のキーのコンポーネントがこない場合キャッシュを消します
* cacheTimeを負の数にした場合、即座にキャッシュを消します
*/
export const promiseToSuspense = <T extends () => Promise<unknown>>(key: Key, promise: T, cacheTime = 100): PromiseResult<T> => {
useEffect(() => {
const cr = result.get(key);
// 同一のキーのコンポーネントが来たので、キャッシュ削除をキャンセル
if (cr && cr.timeoutId !== 0) {
window.clearTimeout(cr.timeoutId);
result.set(key, { timeoutId: 0, value: cr.value });
}
return () => {
// cacheTimeに応じてキャッシュを消す
if (cacheTime >= 0) {
result.set(key, {
timeoutId: window.setTimeout(() => {
result.delete(key);
}, cacheTime),
value: result.get(key)?.value,
});
} else {
result.delete(key);
}
}
}, [cacheTime, key]);
// キーが変更されたとき、前のキーのキャッシュを消す
const keyRef = useRef(key);
if (keyRef.current !== key) {
result.delete(key);
keyRef.current = key;
}
// キャッシュがあるか、promiseが解決されていない場合新しくpromiseを呼ばない
const r = result.get(key);
if (r) return r.value as PromiseResult<T>;
if (waiting.has(key)) throw waiting.get(key);
// promiseを呼び、結果をキャッシュする
const p = promise();
p.then(value => {
result.set(key, { timeoutId: 0, value });
}).finally(() => {
waiting.delete(key);
});
waiting.set(key, p);
throw p;
}
サンプル
const SampleComponent = () => {
const promise = new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 5000));
const result = awaitToSuspense('test', () => promise);
return <div>{result}</div>;
}
const ParentComponent = () =>
<Suspense>
<SampleComponent />
</Suspense>;