Closed6

SWR の Suspense モードの型を調べる

かしかし

やること

TypeScript で React の Suspense を利用するときに、 SWR の型について気になったので調べる。

https://swr.vercel.app/docs/suspense

気になった点

こんな感じで型のついた 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>;
}

https://codesandbox.io/s/react-typescript-swr-tt5byg

前提

SWR のバージョンは 2.0.0-beta.6

かしかし

(英語が得意ではないので解釈が間違ってたらすみません)

SWR のリポジトリに似た議論がある。

https://github.com/vercel/swr/issues/1412

TypeScript で SWR の Suspense モードを利用する場合に次のような型のサポートがあるといいのではないか、という提案をしている

  • key が falsy にならないこと
  • fetchernull にならないこと
  • dataundefined にならないこと

僕が気になったことはこの3つ目と同じ。
(あとの2つがどう嬉しいのかはこのタイミングでよくわからなかったので気が向いたら調べる)

これをやってない理由として、vercel の shuding 氏のコメント は、「SWRConfig の設定が個々のユースケースに伝播するからできないよ」と言ってる。

おそらく、以下のように Profile を定義しても AppSWRConfig によって 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 モードにおいて dataundefined にならないのだから、ちゃんと型をなおしてくれ~
  • 自分のユースケースに合った型を付けたフックを作ったらいいかも
かしかし

自分のユースケースに合った型を付けたフックを作ったらいいかも

これを作ってみようかと思った。
この 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 モードになる)
useSWRSuspense.ts
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 にしてデータ取得を行わないようにする)するケースにおいては、 dataundefined になるらしい

https://swr.vercel.app/docs/suspense#note-with-conditional-fetching

SWR のリポジトリ issue で挙げられていた以下の項目は、 dataundefined になるケースをつぶすのが目的みたい

  • 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(...); する処理を追加してみる。

useSWRSuspense.ts
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 してしまうとコンポーネントがマウントされず、いつまでたっても isReadytrue にならない。(ここに書いていること)

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 をしない」みたいなルールを設けて運用し、さらにテストなんかで動作を保証するのがいいのかなと思った。

自分の欲しい用途だけ対応させた型を用意して、自制心をもって使用する必要があるなと思った。
以上

このスクラップは2022/08/27にクローズされました