27
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React Suspense を試してみた

Last updated at Posted at 2018-08-24

注意: これは実験的な機能なので、v17リリースまでにAPIが変更される可能性が高い。紹介する限り既に unstable_AsyncMode や resource.read のAPIは一度変更されている。


標準版 React の canary ビルドを入れる

yarn add react@canary react-dom@canary
import "@babel/polyfill";
import React from "react";
import ReactDOM from "react-dom";
import { createResource, createCache } from "simple-cache-provider";

// cast any for typescript
const Timeout: React.ComponentType<{ ms: number }> = (React as any).Timeout;
const AsyncMode = (React as any).unstable_AsyncMode;

const cache = createCache();
const hn = createResource(async (id: number) => {
  const url = `https://hacker-news.firebaseio.com/v0/item/${id}.json`;
  const res = await fetch(url);
  await new Promise(resolve => setTimeout(resolve, 400));
  return res.json();
});

const HNStory = (props: { id: number }) => {
  const data = hn.read(cache, props.id);
  return (
    <p>
      {data.title}: {data.url}
    </p>
  );
};

class App extends React.PureComponent {
  render() {
    return (
      <div>
        <h1>Try suspense</h1>
        <button
          onClick={() => {
            this.forceUpdate();
          }}
        >
          reload
        </button>
        <hr />
        <Timeout ms={200}>
          {(didTimeout: boolean) => {
            if (didTimeout) {
              return <span>The content is still loading :(</span>;
            }
            return <HNStory id={8863} />;
          }}
        </Timeout>
      </div>
    );
  }
}

ReactDOM.render(
  <AsyncMode>
    <App />
  </AsyncMode>,
  document.querySelector(".root")
);

挙動

一旦は HNHistory のレンダリングはスキップされる。Hacker News の API 叩く。200ms を超えると Timeout children に与えた renderProp が didMount: boolean の情報を付与して発火し、プレースホルダが表示される。それまでに取得が完了されていれば、そのまま表示される。

reload のボタンを押すと強制リロードされるが、キャッシュ構築済みなのでプレースホルダが出ることなく表示される。

AsyncMode を外した場合、初回レンダリング時から Timeout の children が発火する。

解説

  • AsyncMode でラップすることで、非同期レンダリングを有効化する
  • simple-cache-provider によって、cache と resource を生成する。
    • これは単にリクエスト時の引数に対してその結果をキャッシュするモジェール。なので simple-cache
  • resource.read(cache, key) 時、キャッシュが無ければ throw new Promise(...) で例外処理に飛ぶ
  • Timeout に実装された ErrorBoundary によって、catch される https://reactjs.org/docs/error-boundaries.html
  • resource.read(cache, key) 時、キャッシュが構築されていれば動機的に返却する
  • 正常処理を行う

Reactの render は同期でなければいけない、という縛りを、見た目上同期APIに見えつつ Promise を throw して ErrorBoundary で catch してフォールバックすることで実現している。なので、v17以降では、 render の処理は最後まで走らないことがある というのを認識しないといけない。さすがに、ここで副作用を起こすコードを書く人はいないはずだが…。

要は、Reactの世界観の中では、ライブラリ作者はレンダリングの優先度付けのために、自分で componentDidCatch を実装したカスタムな ErrorBoundary を使って、その復帰処理で好き勝手してもよいということになる。

ライブラリ作者は、行儀よくライブラリの邪魔をしないErrorを裏で生成しないといけない。うまくやらないと治安悪いライブラリ増えそうな気がする。

27
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?