はじめに
この記事は、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 された場合
「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 結果を表示
-
Promise が throw された場合
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 によってこのようなパターンの開発体験を向上していけたらいいなと思います!
参考文献
- 公式サイト
- RFC
- 偉大なる uhyo さんの記事
- その他