Open5

Render-as-you-fetchってなに??

msy.msy.

Render-as-you-fetchとは

まず、ReactのSuspenseは従来のデータフェッチのアプローチとは異なるアプローチをとっている。
それがRender-as-you-fetchというデータフェッチのアプローチ方法である。

このRender-as-you-fetchというアプローチを知るには、従来のデータフェッチのアプローチについて知る必要がるため、軽くまとめる。

従来のデータフェッチのアプローチにはRender-as-you-fetchを含め、以下のようなものがある。

  1. Fetch-on-render → 例:useEffect内でのフェッチ
  2. Fetch-then-render → 例:Relayを使ったデータフェッチ(suspenseは使わない)
  3. Render-as-you-fetch → 例:Suspenseでのデータフェッチ

個々の詳細の説明については別スクラップで。

msy.msy.

Fetch-on-render

このデータフェッチのアプローチは昨今のReactでのデータフェッチではよく見るやつ。
useEffect内でデータフェッチを行う非同期関数を呼び出すみたいなタイプ。

サンプルコード
(※クラスコンポーネントの方もcomponentDidMountとか書いてあったけどめんどいから省略)

// in function component
useEffect(() => {
  fetchSomething();
}, []);

このアプローチ方法がFetch-on-renderと呼ばれるのは、画面上にコンポーネントがレンダリングされてからデータフェッチが行われるからである。
(ここでいうレンダリングはブラウザレンダリング??????な気がする)

以下のようなProfilePageProfileTimeLineという二つのコンポーネントを例に考えてみる。

function ProfilePage() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser().then(u => setUser(u));
  }, []);

  if (user === null) {
    return <p>Loading profile...</p>;
  }
  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline />
    </>
  );
}

function ProfileTimeline() {
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    fetchPosts().then(p => setPosts(p));
  }, []);

  if (posts === null) {
    return <h2>Loading posts...</h2>;
  }
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

上から順にデータフェッチが行われるため、Fetch-on-renderはウォーターフォール と呼ばれたりする。
(並列処理的にデータフェッチされるのではなく、シーケンスとなる。)

msy.msy.

Fetch-then-render

Reactのデータフェッチで、ライブラリを使用することでFetch-on-renderのデータフェッチで問題視された「ウォーターフォール」を解決することもできる。(例えばRelayなど)

※Reactの公式では、Relayの知識がなくても理解できるようにあえてRelayを使用していなかった。

Fetch-on-renderでは、とあるデータフェッチが完了するまで次のデータフェッチが出来ずシーケンスになっていたことが問題になっていた。

そこで、複数のPromiseを受け取り、全てのPromiseがrejectされずにresolveされた場合に一つのPromiseを返すPromise.allを使用する。

要するにPromiseを使用することで、データ取得してからコンポーネントをrenderさせるようにすることができる。

/*
 *  fetchUserとfetchPostsは非同期Fetch関数
 * (おそらくPromise<User>やPromise<Post[]>を返すと思われる)
 * */
function fetchProfileData() {
  return Promise.all([
    fetchUser(),
    fetchPosts()
  ]).then(([user, posts]) => {
    return {user, posts};
  })
}

「レンダリング前にfetchする」(Fetch-then-render)パターンでは必要なデータの取得を上位コンポーネントにまとめ、そこでPromise.allしてデータ取得ができてから下位のコンポーネントをレンダリングする。

→ Next.jsのgetInitialProps/getServerProps/getStaticPropsがイメージとして近そう

// なるはやでデータフェッチが実行される
const promise = fetchProfileData();

function ProfilePage() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    promise.then(data => {
      setUser(data.user);
      setPosts(data.posts);
    });
  }, []);

  if (user === null) {
    return <p>Loading profile...</p>;
  }
  return (
    <>
      <h1>{user.name}</h1>
      <ProfileTimeline posts={posts} />
    </>
  );
}

// 子供コンポーネント側でデータフェッチすることはない
function ProfileTimeline({ posts }) {
  if (posts === null) {
    return <h2>Loading posts...</h2>;
  }
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
msy.msy.

Render-as-you-fetch

Render-as-you-fetchは必要なデータ取得を上位にまとめる意味ではRender-then-fetchと同じだが、Promise.allをしないで、個々のPromiseを取得・回すようなイメージ。

このデータフェッチパターンでは、Suspenseを使用することでデータフェッチ完了前にレンダリングを開始することができる。

【サンプルコード】

// この関数はPromiseじゃない?らしい
const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // この時点でuserのデータは取得できない可能性がある
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
   // この時点でuserのデータは取得できない可能性がある
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

ただし複数のPromiseを取り回すケースでは、コードが複雑になりがちになる。
そこでSuspenseを使用することで、コンポーネントのデータ依存と優先度を宣言的に記述することができる。

Render-as-you-fetchパターンを実現するには、データの取得をSuspenseが置かれている位置よりも上に引き上げる必要がある。

実際どこにしたいのかと言うと、ページ遷移によって発生するデータ取得をuseTransitionで処理したいので、個々のページよりも上、ルートでやる必要がある。これを何とかして個々のコンポーネント単位に収めたいわけだが…

とりあえずページ単位でのデータ取得までの分割は比較的簡単にできて、これはNext.jsを参考にページ定義単位でgetInitialPropsを書くようにすることで実現できると思う。