Next.js でConcurrent Rendering(Suspense)を試してみた

2022.01.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

現在(2022年1月14日)ではReact18のConcurrent RenderingはExperimentalな機能なので、リリースされるころには変わっている可能性がありますが、素振りやコンセプトは体験しておこうと思いまして、GitHubのユーザ検索を作ってみることにしました

React18のConcurrent Renderingの概要はこちらを参考にしました。大変わかりやすい説明で感謝です。

公式はこちらになります

サンプルを簡単につくるために、Next.jsを使っています。素のReactでも良いんですが、Routerやらビルド周りを設定するのが面倒なので今回はNext.jsで作ります。

手順

Next.jsのTypeScriptでのプロジェクトを作成を参考して作っています。

1. Next.jsのプロジェクトを新規作成

$ npx create-next-app@latest --ts

✔ What is your project named? … react-suspend-sample // 適当なプロジェクト名

公式だと空のtsconfig.jsonを作成しているけど、今やってみるとすでに存在しているのでスキップしました。

一旦起動してみる。

$ cd react-suspend-sample
$ npm run dev

> react-suspend-sample@ dev /Users/kamei.hidetoshi/workspace/react-suspend-sample
> next dev

ready - started server on 0.0.0.0:3000, url: http://localhost:3000

http://localhost:3000 をブラウザで開く。

2. React18 Concurrent Renderingを有効化する

こちらを参考に

$ npm install next@latest react@rc react-dom@rc

3.Suspenseを使ってみる

サンプルなので、1ファイル内で完結させちゃいますがご了承ください。

全体をSuspenseでくくっちゃいます。Suspense 内で throw Promise されたときにキャッチするので、最悪キャッチできるように最上位のコンポネートで括っておいたほうが無難では?ぐらいの意図です。現状あまりSuspenseでのアーキテクチャのベストプラクティスがいまいちわかっていない状態なので、ご注意ください。

import "../styles/globals.css";
import type { AppProps } from "next/app";
import { Suspense } from "react";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Suspense fallback={<div>loading</div>}>
      <Component {...pageProps} />
    </Suspense>
  );
}

export default MyApp;

4.Homeの中身をごっそり消す

わかりやすいように、一旦 Homeの中身をごっそり消しておきます。

import type { NextPage } from "next";
const Home: NextPage = () => {
  return (
    <div>
      <h1>GitHub Users</h1>
    </div>
  );
};
export default Home;

5.GitHub APIのusersを取得する

GitHub Apiのドキュメントを参考にしてAPI取得を部分を作る。型は今回必要な分だけ抜粋しました。

pages/index.tsx に追記します。

type GitHubUser = {
  id: string;
  name: string;
  avatar_url: string
  url:string
};

function githubUsersApi(): Promise<GitHubUser[]> {
  return fetch("https://api.github.com/users").then((res) => res.json());
}

6. users取得をthrowします。

こちらでも紹介されている通り、外部にデータを持つことにしました。

import type { NextPage } from "next";

type GitHubUser = {
  id: string;
  login: string;
  avatar_url: string;
  html_url: string;
};

function githubUsersApi(): Promise<GitHubUser[]> {
  return fetch("https://api.github.com/users").then((res) => res.json());
}

let users: GitHubUser[] | undefined;

const Home: NextPage = () => {
  if (!users) {
    throw githubUsersApi().then((res) => {
      users = res;
    });
  }

  return (
    <div>
      <h1>GitHub Users</h1>
      <ul>
        {users?.map((user) => {
          return (
            <li key={user.id}>
              <a href={user.html_url}>
                <img src={user.avatar_url} width={100} height={100}/>
                {user.login}</a>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

export default Home;

まとめ

useEffect を使ったサンプルより、すごいシンプルになっている。ローディング処理がこれだけで良いのか感があるけどシュッとしたコードになりました。

次回は、エラー処理やリロードをどうやって実装するのか考えみます。

今回のサンプルはこちらにリポジトリになります。