LoginSignup
19
9

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

Last updated at Posted at 2023-08-04

はじめに

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

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

Suspense

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

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

React16.6 までの Suspense

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

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

Suspense for Dynamic import

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

React 18 以降の Suspense

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

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

Suspense for Data Fetching

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

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 を用意します。

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

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 で使用します。

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 でしていました。

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/)

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 によってこのようなパターンの開発体験を向上していけたらいいなと思います!

参考文献

19
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
9