Masayan tech blog.

  1. ブログ記事一覧>
  2. React18からはSuspenseを使ってデータを取得/描画しよう

React18からはSuspenseを使ってデータを取得/描画しよう

公開日

Suspenseとは

SuspenseはReact ver18で実装された外部APIなどからのデータ取得状態を検知するコンポーネント機能です。

従来の方法とそのデメリット

Suspenseが登場するまでは、外部APIなどからのデータ取得し描画するためには、useEffect内で非同期処理を実行し、それをuseStateで保持する方法が一般的でした。

ただし、この方法だと以下のようなつらさがあります

  • 副作用(関数コンポーネントのレンダリングに関係ない処理)を起こす目的で用意されているuseEffectは極力使いたくないものだが、データ取得のためだけにわざわざ使わないといけない
  • 取得したデータをJSX内で使用するという目的だけのためだけにわざわざstateを用意しないといけない
  • useEffect内で非同期処理を実行し、それをstateにセットして・・・という一連の処理がどうしても命令的(手続き的)になりがちで可読性が損なわれやすい

実装例

従来の方法

todo一覧を表示するページを例に、従来までの方法とSuspenseを使った実装サンプルを紹介します。

Home.tsxからTodos.tsxコンポーネントを呼び出しTODO一覧を表示しています

index.tsx

import Todos from '../components/Todos';

export default function Home() {
  return <Todos />;
}

Todos.tsx

import { useCallback, useEffect, useState } from 'react';

export type Todo = {
  completed: boolean;
  id: number;
  title: string;
  userId: number;
};

export default function Todos() {
  const [todos, setTodos] = useState<Todo[]>([]);

  const fetchTodos = useCallback(async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos');
    const data = await response.json();
    return data;
  }, []);


  useEffect(() => {
    // ここでローディングなど表示のための処理をする
    fetchTodos().then((todos) => {
      setTodos(todos);
    });
  }, []);

  return (
    <ul>
      {todos.map((todo, i) => (
        <li key={i}>{todo.title}</li>
      ))}
    </ul>
  );
}

Suspenseを使った方法

Suspenseを利用する場合、手順は以下の通りです。

  • データ描画用のコンポーネントを使う側
    • Suspenseコンポーネントで、データの取得/描画を行うコンポーネントをラップする
    • Suspenseコンポーネントには、fallback属性として、データ取得が完了するまでの間に表示したい要素やコンポーネントを指定する(ローディングコンポーネントなど)

index.tsx

import { Suspense } from 'react';
import Todos from '../components/Todos';

export default function Home() {
  return (
    <Suspense fallback={<p>...loading</p>}>
      <Todos />
    </Suspense>
  );
}

Todos.tsx

  • サスペンドされるコンポーネント側
    • コンポーネントの外側でデータ格納用の変数を定義しておき、このコンポ-ネントがレンダリングされたらデータフェッチ用の関数が実行されるようにする
    • データがまだ取得されていなければ取得用fetch関数が実行される。この際、fetch関数の実行結果であるPromiseをthrowするようにする
    • Promiseをthrowすると、Todo.tsxのレンダリングがサスペンド(中断)され、fallbackに指定した要素がレンダリングされる。
    • throwされたPromiseがresolveされるとサスペンドが解除され、Suspenseでラップしたコンポーネントが再レンダリングされる
    • Promiseがrejectされた場合はErrorBoundaryがキャッチするようにしておくこと
export type Todo = {
  completed: boolean;
  id: number;
  title: string;
  userId: number;
};


let todos: Todo[] | undefined;


export default function Todos() {
  const fetchTodos = async (): Promise<Todo[]> => {
    return fetch('https://jsonplaceholder.typicode.com/todos').then((res) =>
      res.json()
    );
  };


  if (!todos) {
    throw fetchTodos().then((data) => (todos = data));
  }


  return (
    <ul>
      {todos.map((todo, i) => (
        <li key={i}>{todo.title}</li>
      ))}
    </ul>
  );
}

従来までの命令的な記述方法と比べ、非同期処理をより宣言的に書けるようになり、宣言的UIというReactの根底にある思想とマッチするようになりました。

複数コンポーネントがある場合は、以下のようにSuspenseはネストさせることも可能です。

<Suspense fallback={<Loading />}>
   <Users />
   <Suspense fallback={<Loading />}>
     <Todos />
   </Suspense>
</Suspense>

まとめ

いかがでしたでしょうか。本記事では、React18から登場したSuspenseを使って非同期処理を実行しデータを取得/描画する方法について紹介しています。Suspenseを使うことにより、より宣言的に処理を記述できるようになったので、是非参考にしてみてください