React18の新機能 Suspense機能を使ってローディング画面とエラー画面を表示してみよう

# はじめに
こんにちは、都内でソフトウェアエンジニアをしている[YSasago](https://qiita.com/YSasago)と申します。
React18で、Suspense機能が追加されました。そこで、Suspense機能についてまとめようかと思います。

* Suspense機能を使うことで、API実行時などのローディング状態の記述を上位のコンポーネントで宣言できる
* Suspense機能を適切に分割して使うことでユーザービリティの向上をつなげることができる

> サスペンスにより、コンポーネントツリーの一部がまだ表示できない場合に、ロード中という状態を宣言的に記述できるようになります：

https://ja.reactjs.org/blog/2022/03/29/react-v18.html#new-suspense-features


# サンプルアプリの作成
Suspense機能を試すために、[jsonplaceholder](https://jsonplaceholder.typicode.com/)の[albums](https://jsonplaceholder.typicode.com/albums)を取得して、画面に表示するサンプルアプリを作成しました。

https://jsonplaceholder.typicode.com/

![FireShot Capture 116 - React App - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95954/82eee050-81ec-f2b4-49cc-12adfedde0b3.png)


```typescript:src/index.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import reportWebVitals from "./reportWebVitals";

const queyClient = new QueryClient();

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queyClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

reportWebVitals();

```

```typescript:src/App.tsx
import "./App.css";
import { ReactQuery } from "./components/ReactQuery";

function App() {
  return (
    <div className="App">
      <ReactQuery />
    </div>
  );
}

export default App;

```

```typescript:src/components/ReactQuery.tsx
import { useQuery } from "@tanstack/react-query";
import axios from "axios";

type Album = {
  userId: number;
  id: number;
  title: string;
};
const fetchAlbums = async () => {
  const result = await axios.get<Album[]>(
    "https://jsonplaceholder.typicode.com/albums"
  );
  return result.data;
};
export const ReactQuery = () => {
  const { isLoading, error, data } = useQuery<Album[]>(["albums"], fetchAlbums);

  if (error) return <p>error</p>;
  if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      <p>ReactQuery</p>
      {data?.map((album) => (
        <p key={album.id}>{album.title}</p>
      ))}
    </div>
  );
};
```

# 読込中の画面表示を、Suspense機能を使って実装してみる
useQueryのisLoadingでローディング画面を実装していた箇所を、Suspense機能を使うように変更します。
[react-query](https://tanstack.com/query/v4/docs/guides/suspense)のライブラリは、Suspense機能に対応しているので、有効化するだけで使用することができます。

https://tanstack.com/query/v4/docs/guides/suspense

```typescript:src/App.tsx
import { Suspense } from "react"; // 追加
import "./App.css";
import { ReactQuery } from "./components/ReactQuery";

function App() {
  return (
    <div className="App">
      {/* fallbackにはローディング中に表示する画面を渡す*/}
      <Suspense fallback={<p>Loading...</p>}>
        <ReactQuery />
      </Suspense>
    </div>
  );
}

export default App;
```

```typescript: src/components/ReactQuery.tsx
import { useQuery } from "@tanstack/react-query";
import axios from "axios";

type Album = {
  userId: number;
  id: number;
  title: string;
};
const fetchAlbums = async () => {
  const result = await axios.get<Album[]>(
    "https://jsonplaceholder.typicode.com/albums"
  );
  return result.data;
};
export const ReactQuery = () => {
  const { isLoading, error, data } = useQuery<Album[]>(
    ["albums"],
    fetchAlbums,
    { suspense: true } // 追加: Suspense機能を有効化する
  );

  // if (error) return <p>error</p>;
  // if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      <p>ReactQuery</p>
      {data?.map((album) => (
        <p key={album.id}>{album.title}</p>
      ))}
    </div>
  );
};
```


Suspense機能を使ってローディング画面を作成することができました。
![スクリーンショット 2022-11-06 17.25.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95954/46ea480c-f0fe-b861-222e-8a955f6bd819.png)



# エラーの画面表示を、Suspense機能を使って実装してみる
エラーの画面表示を、Suspense機能を使って実装しようと思います。

最初に、ライブラリの[react-error-boundary](https://www.npmjs.com/package/react-error-boundary)を追加します。

https://www.npmjs.com/package/react-error-boundary

```zsh
$ yarn add react-error-boundary
```

```typescript:src/App.tsx
import { Suspense } from "react";
import "./App.css";
import { ReactQuery } from "./components/ReactQuery";
import { ErrorBoundary } from "react-error-boundary"; // 追加

function App() {
  return (
    <div className="App">
      {/* fallbackにエラー画面のUIを渡してあげる */}
      <ErrorBoundary fallback={<p>error</p>}>
        <Suspense fallback={<p>Loading...</p>}>
          <ReactQuery />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

export default App;
```

```typescript:src/components/ReactQuery.tsx
import { useQuery } from "@tanstack/react-query";
import axios from "axios";

type Album = {
  userId: number;
  id: number;
  title: string;
};
const fetchAlbums = async () => {
  const result = await axios.get<Album[]>(
    "https://jsonplaceholder.typicode.com/albumsあ" // エラーが表示されるように無効なURLを指定する
  );
  return result.data;
};
export const ReactQuery = () => {
  const { data } = useQuery<Album[]>(["albums"], fetchAlbums);

  return (
    <div>
      <p>ReactQuery</p>
      {data?.map((album) => (
        <p key={album.id}>{album.title}</p>
      ))}
    </div>
  );
};
```

Suspense機能を用いて、エラー画面が表示されました。
![スクリーンショット 2022-11-06 17.42.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95954/8c30bb63-847c-a853-676a-440d561dbda5.png)

# 複数のSuspenseを使う場合
複数のSuspenseを使って、書くとユーザービリティの向上につながります。
よって、下記のコードの様にコンポーネントごとに、適切に```Suspense```と```ErrorBoundary```と書くと良いでしょう。
```typescript
export const ReactQuery = () => {
  return (
    <div style={{ display: "flex", padding: "16px" }}>
      <Sidebar />
      <div style={{ flexGrow: 1 }}>
        {/* コンポーネント毎にErrorBoundaryとSuspenseを使う */}
        <ErrorBoundary fallback={<p>Album list error</p>}>
          <Suspense fallback={<p>Album list Loading...</p>}>
            <AlbumList />
          </Suspense>
        </ErrorBoundary>
        {/* コンポーネント毎にErrorBoundaryとSuspenseを使う */}
        <ErrorBoundary fallback={<p>Todo list error</p>}>
          <Suspense fallback={<p>Todo list Loading...</p>}>
            <TodoList />
          </Suspense>
        </ErrorBoundary>
      </div>
    </div>
  );
};
```

:::note info
複数のSuspenseを使う場合は、ネストが深いものが最優先されます。
:::


# まとめ
* React18で追加されたSuspense機能を手元で動かしてみました
* Suspense機能を導入するとはユーザービリティの向上につながるので、プロジェクトに合わせて導入したいですね

# 参考

https://www.udemy.com/course/react_v18/