SWR の Suspense モードの型を調べる
やること
TypeScript で React の Suspense を利用するときに、 SWR の型について気になったので調べる。
気になった点
こんな感じで型のついた fetcher を用いた SWR の利用例を考える
// fetcher: () => Promise<Data>
const { data } = useSWR(key, fetcher, { suspense: true });
data
の型が Data | undefined
となるけど、公式の Suspense モードの使用例を見ると undefined
にはならないでほしいなあと思った。
なので実際には、オプショナルチェーンを使ってこんな感じで書かないといけない
function Profile() {
const { data } = useSWR("/api/user", fetcher, { suspense: true });
return <div>hello, {data?.name}</div>;
}
前提
SWR のバージョンは 2.0.0-beta.6
(英語が得意ではないので解釈が間違ってたらすみません)
SWR のリポジトリに似た議論がある。
TypeScript で SWR の Suspense モードを利用する場合に次のような型のサポートがあるといいのではないか、という提案をしている
-
key
が falsy にならないこと -
fetcher
がnull
にならないこと -
data
がundefined
にならないこと
僕が気になったことはこの3つ目と同じ。
(あとの2つがどう嬉しいのかはこのタイミングでよくわからなかったので気が向いたら調べる)
これをやってない理由として、vercel の shuding 氏のコメント は、「SWRConfig
の設定が個々のユースケースに伝播するからできないよ」と言ってる。
おそらく、以下のように Profile
を定義しても App
の SWRConfig
によって Suspense モードで使うことが定められているならば型の付けようがないということを言っているのだと思う。
また、以下の NonSuspenseApp
を使えば同じコンポーネントでも Suspense モードでない状態で使うこともできる。
function Profile() {
const { data } = useSWR("/api/user", fetcher);
return <div>hello, {data?.name}</div>;
}
function App() {
return (
<SWRConfig value={{ suspense: true }}> // Profile の useSWR が Suspense モードになる
<Suspense fallback={<div>loading...</div>}>
<Profile />
</Suspense>
</SWRConfig>
);
}
function NonSuspenseApp() {
return (
<SWRConfig value={{ suspense: false}}> // Profile の useSWR が Suspense モードにならない
<Suspense fallback={<div>loading...</div>}>
<Profile />
</Suspense>
</SWRConfig>
);
}
その他話されていることは以下のようなもので完全同意した。
- Suspense モードを従来の SWR のフックと別物として扱ってはどうか(non-Suspense の場合との型の問題を気にしなくて済む、実装が分離されてシンプルになる)
- 今の実装では Suspense モードにおいて
data
がundefined
にならないのだから、ちゃんと型をなおしてくれ~ - 自分のユースケースに合った型を付けたフックを作ったらいいかも
自分のユースケースに合った型を付けたフックを作ったらいいかも
これを作ってみようかと思った。
この issue で扱われてた問題のうち、現時点で僕が問題に感じているのは data
の型だけ。
これが解決されれば現時点ではよしとする。
また、 useSWR
のインターフェース SWRHook
はいくつかの定義があるので、普段よく使う次の2つの定義のみに対応する。
<Data = any, Error = any>(key: Key, fetcher: BareFetcher<Data> | null): SWRResponse<Data, Error>;
<Data = any, Error = any>(key: Key, fetcher: BareFetcher<Data> | null, config: SWRConfiguration<Data, Error, BareFetcher<Data>> | undefined): SWRResponse<Data, Error>;
以下のように実装して、次の2つが満たされた。
-
data
の型がData | undefined
からData
に直された -
config
で Suspense モードの上書きができなくなった(強制的に Suspense モードになる)
import useSWR, { Key, BareFetcher, SWRConfiguration, SWRResponse } from "swr";
// キーが data であるプロパティの値の型が undefined でなくなるようにする
type NonUndefinedData<T> = T extends {
data?: undefined | infer Data;
}
? { data: Data } & Omit<T, "data">
: never;
type SWRSuspenseResponse<Data, Error> = NonUndefinedData<
SWRResponse<Data, Error>
>;
// Configuration で 'suspense' モードの指定をできなくする
type SWRSuspenseConfiguration<
Data,
Error,
Fn extends BareFetcher<any> = BareFetcher<any>
> = Omit<SWRConfiguration<Data, Error, Fn>, "suspense">;
export const useSWRSuspense = <Data, Error>(
key: Key,
fetcher: BareFetcher<Data> | null,
config?: SWRSuspenseConfiguration<Data, Error, BareFetcher<Data>> | undefined
): SWRSuspenseResponse<Data, Error> => {
const { data, ...others } = useSWR(key, fetcher, {
...config,
suspense: true
});
return {
// `data` が `undefined` として扱われないようにする型アサーション
// Suspense モードにおいて `data` が `undefined` にならないから問題ない
data: data as Data,
...others
};
};
このフックを使って、冒頭の Profile
を次のように書くことができた。
function Profile() {
const { data } = useSWRSuspense("/api/user", fetcher);
// `data` が `undefined` ではないのでオプショナルチェーンを使わなくていい
return <div>hello, {data.name}</div>;
}
Conditional Fetching(条件によって key を null
にしてデータ取得を行わないようにする)するケースにおいては、 data
が undefined
になるらしい
SWR のリポジトリ issue で挙げられていた以下の項目は、 data
が undefined
になるケースをつぶすのが目的みたい
- key が falsy にならないこと
実際、上のコードで key に null
を当ててみるとエラーになる。
function Profile() {
const [isReady, setReady] = useState(false);
const { data } = useSWRSuspense(
isReady ? "/api/user/suspense" : null, // Promise を throw しない
fetcher
);
useEffect(() => {
setReady(true);
}, []);
// `data` が `undefined` なのでここで `TypeError, Cannot read properties of undefined (reading 'name')` になる
return <div>hello, {data.name}</div>;
}
なので、 useSWRSuspense
において、 key が undefined
のときには throw new Promise(...);
する処理を追加してみる。
export const useSWRSuspense = <Data, Error>(
key: Key,
fetcher: BareFetcher<Data> | null,
config?: SWRSuspenseConfiguration<Data, Error, BareFetcher<Data>> | undefined
): SWRSuspenseResponse<Data, Error> => {
if (!key) {
throw new Promise(() => {});
}
// 以下同様の実装
};
しかし、これもうまくいかない。
2行目で Promise を throw してしまうとコンポーネントがマウントされず、いつまでたっても isReady
が true
にならない。(ここに書いていること)
const [isReady, setReady] = useState(false);
const { data } = useSWRSuspense(isReady ? "/api/user" : null, fetcher);
useEffect(() => { setReady(true); }, []);
Conditional Fetching の例として挙げられているようなケースにおいては、 Suspense モードではない SWR と併用するといいのかなあなどと思った
function MyProjects () {
const { data: user } = useSWRSuspense('/api/user', userFetcher)
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id, projectsFetcher)
return (
<div>
<p>{user.name}</p>
{project ? <div>{`${project.length} projects`}</div> : <div>loading projects...</div>}
</div>
);
}
でもこうすると、 user
を Suspense モードにするメリットなくない?という気がした。
結論
型を適当に定めたらいいのでは?という気持ちで使い始めたけど、既存の SWR の機能に Suspense を型の制約をつけるだけだと意図せず不安定な動作を引き起こしかねないらしい。
現状、調べたわかったことは SWRConfig の設定がうまく反映させられなかったり、Conditional Fetching ができなかったりするが、ほかにもあるかもしれない。
例えば「Suspense モードのときは Conditional Fetching をしない」みたいなルールを設けて運用し、さらにテストなんかで動作を保証するのがいいのかなと思った。
自分の欲しい用途だけ対応させた型を用意して、自制心をもって使用する必要があるなと思った。
以上