React18 の Suspense は、これまでの Suspense と何が違うの？

# はじめに

この記事は、Qiita株式会社の社内勉強会にて発表した React18 の Suspense の新機能についてまとめた記事です！

社内勉強会に関する以下の記事もよかったら読んでみてください！！

https://qiita.com/wataru86/items/4681d824c06cb0bba1ec

# Suspense

https://react.dev/blog/2022/03/29/react-v18

React18 の新機能として、Suspense がよく話題に上がりますが、 Suspense 自体は React16.6で追加された機能です。一体どのようなことができるようになったのでしょうか？

本記事では React16.6 追加された時点での Suspense を軽く振り返り、その後 React 18 以降の Suspense で何ができるようになったか紹介します。

## React16.6 までの Suspense

Suspense が追加された当時は Dynamic import のために Suspense を使う方法のみ公式でサポートされていました。詳細は以下記事にまとめてあります。

https://qiita.com/wataru86/items/601232fb8ebab4251466

まとめると、以下のようになります。

:::note

**Suspense for Dynamic import**

Code Splitting により分割されたコンポーネントの import 中は fallback を表示し、import が完了したら Suspsense 内を表示する。

:::

## React 18 以降の Suspense

React 18 で Suspense が正式版になりました。 React 16.6 と具体的にどう違うかは RFC に記載されていますが、詳細は割愛します。

https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md

本記事では特に React 18 で正式対応となった、Dynamic import **以外**の使用方法 (Suspense for Data Fetching) について紹介します。本記事の執筆にあたり、[`@uhyo`](https://qiita.com/uhyo) さんの数々の記事がとても参考になりました。

https://zenn.dev/uhyo/books/react-concurrent-handson

https://qiita.com/uhyo/items/bbc22022fe846fd2b763

https://qiita.com/uhyo/items/255760315ca61544fe33

# Suspense for Data Fetching

一言で説明すると、こんな感じです。

:::note
**Suspense for Data Fetching**

非同期処理の実行中は fallback を表示し、非同期処理が完了したら Suspsense 内を表示する。
:::

この、「非同期処理の実行中」や「非同期処理の完了」は Promise を使って検知します。

以下のような処理の順番になります。

- `Suspsense` を含むコンポーネントが render される際に、 `Suspsense` 内の render が試される
    - **Promise が throw** された場合
        - Promise を実行して完了するまで fallback を表示
        - Promise が完了したら、再び Suspsense 内の render を試す
    - 問題なければ render 結果を表示

**「Promise を throw する」** というのが特に重要なポイントですね。

## 実装例

この新しい Suspense の使用には、Promise を throw するコンポーネントを作る必要があります。
最初は Promise を throw し、その Promse が解決したら 結果の値を返す `fetchPosts` を用意します。

以下サイトの例を参考にしました。

https://dev.to/smitterhane/swap-out-useeffect-with-suspense-for-data-fetching-in-react-2leb

```tsx
let status = "pending"
let result

function fetchPosts(userId) {
  let url = `https://jsonplaceholder.typicode.com/posts${userId ? "?userId=" + userId : ""}`
  let fetching = fetch(url)
    .then((res) => res.json())
    .then((success) => {
      status = "fulfilled"
      result = success
    })
    .catch((error) => {
      status = "rejected";
      result = error;
    });

  return () => {
    if (status === "pending") {
      throw fetching // Promise を throw
    } else if (status === "rejected") {
      throw result
    } else if (status === "fulfilled") {
      return result // Promise が解決したら、取得したデータを返す
    }
  }
}
```

そして、 `fetchPosts` を使ったコンポーネントを作成し、`Suspense` で使用します。

```tsx
const Posts = () => {
  const userId = JSON.parse(localStorage.getItem("authenticatedUser"))?.id
  const posts = fetchPosts(userId)

  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "10px" }}>
      {posts.map((post, idx) => (
        <Post post={post} key={idx} />
      ))}
    </div>
  )
}

const MyPosts = () => {
  return (
    <Suspense fallback={<Spinner />}>
      <Posts />
    </Suspense>
  )
}
```

実装としてはこのようになると思います。そうすることで、先ほど説明したように、非同期処理の実行中は fallback を表示し、非同期処理が完了したら Suspsense 内を表示する、という処理が Suspense を使って実現できます。

(再掲) 処理の順番

- `Suspsense` を含むコンポーネントが render される際に、 `Suspsense` 内の render が試される
    - **Promise が throw** された場合
        - Promise を実行して完了するまで fallback を表示
        - Promise が完了したら、再び Suspsense 内の render を試す
    - 問題なければ render 結果を表示

## Suspense で非同期処理ができるようになるとどうなる？

これまで非同期処理を実装したい場合は以下のように useState で状態を管理して useEffect でしていました。

```tsx
function Posts() {
  const [isLoading, setIsLoading] = useState(true)
  const [posts, setPosts] = useState([])

  useEffect(() => {
    setIsLoading(true)
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((response) => response.json())
      .then((json) => setPosts(json))
      .finally(() => setIsLoading(false))
  }, [])

  if (isLoading) { return <Spinner /> }

  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: "10px" }}>
      {posts.map((post, idx) => (
        <Post post={post} key={idx} />
      ))}
    </div>
  )
}
```

これに対し、Suspense を使えば先ほどの実装例で示したように、 useState や useEffect を使わずに実装できます。しかし、`fetchPosts` のような「最初は Promise を throw し、その Promse が解決したら 結果の値を返す関数」が必要です。以下のような wrapper があると実装でしやすくなるらしいです。 (参考: https://blog.logrocket.com/react-suspense-data-fetching/)

```ts
function wrapPromise(promise) {
  let status = 'pending'
  let response

  const suspender = promise.then(
    (res) => {
      status = 'success'
      response = res
    },
    (err) => {
      status = 'error'
      response = err
    },
  )

  const read = () => {
    switch (status) {
      case 'pending':
        throw suspender
      case 'error':
        throw response
      default:
        return response
    }
  }

  return { read }
}

export default wrapPromise
```

非同期でデータを fetch して、読み込み後 render する、といったパターンは様々な場面で出現すると思うので、今後 Suspense によってこのようなパターンの開発体験を向上していけたらいいなと思います！

# 参考文献

- 公式サイト
    - [React v18.0 – React](https://react.dev/blog/2022/03/29/react-v18)
- RFC
    - [rfcs/text/0213-suspense-in-react-18.md at main · reactjs/rfcs](https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md)
- 偉大なる uhyo さんの記事
    - [ReactのSuspense対応非同期処理を手書きするハンズオン](https://zenn.dev/uhyo/books/react-concurrent-handson)
    - [React 18に備えるにはどうすればいいの？　5分で理解する - Qiita](https://qiita.com/uhyo/items/bbc22022fe846fd2b763)
    - [Promiseをthrowするのはなぜ天才的デザインなのか - Qiita](https://qiita.com/uhyo/items/255760315ca61544fe33)
- その他
    - [How to use Suspense for data fetching in React - DEV Community](https://dev.to/smitterhane/swap-out-useeffect-with-suspense-for-data-fetching-in-react-2leb)
    - [Experimental React: Using Suspense for data fetching - LogRocket Blog](https://blog.logrocket.com/react-suspense-data-fetching/)