はじめに
React Router v7 がリリースされましたね。わたしが所属する JISOU コミュニティでも話題になっており遊んでみました!この記事では初学者ならではの遊んでみた気付きを共有します!
やったことは
- suspense を使ったローディング
です。
やらないことは
- remix の解説
- react router v7 の解説
- react router v6 との比較
- react router ベストプラクティス
です。
プロジェクト作成
npx create-react-router@latest enjoy-suspense
たくさんファイル、ディレクトリを作成しますが app/routes ディレクトリにコンポーネントを作成して routes.ts を更新します。
コンポーネント
PostsPage コンポーネントは Posts コンポーネントを内包しており、Posts コンポーネントは use
を使って API から取得したデータを描画しています。全体を確認するには コンポーネント全体
を確認してください。
export default function PostsPage({ loaderData }: Route.ComponentProps) {
let { postData } = loaderData;
return (
<div>
<h1>Streaming example</h1>
<Suspense fallback={<h2>🌀 Loading...</h2>}>
<Posts postData={postData} />
</Suspense>
</div>
)
}
export function Posts({ postData }: { postData: Promise<Post[]> }) {
const posts = use(postData)
return (
<ul>
{posts.map(post => (
<li key={post.id}>
{post.title} ({post.body})
</li>
))}
</ul>
);
}
コンポーネント全体
import { Suspense, use } from "react";
import type { Route } from "./+types/posts";
type Post = {
userId: number;
id: number;
title: string;
body: string;
}
export async function loader({}: Route.LoaderArgs) {
const promise = new Promise<Post[]>((resolve, reject) => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json()
})
.then(data => {
setTimeout(() => {
resolve(data);
}, 3000);
})
.catch(error => {
reject(error)
})
});
return { postData: promise }
}
export default function PostsPage({ loaderData }: Route.ComponentProps) {
let { postData } = loaderData;
return (
<div>
<h1>Streaming example</h1>
<Suspense fallback={<h2>🌀 Loading...</h2>}>
<Posts postData={postData} />
</Suspense>
</div>
)
}
export function Posts({ postData }: { postData: Promise<Post[]> }) {
const posts = use(postData)
return (
<ul>
{posts.map(post => (
<li key={post.id}>
{post.title} ({post.body})
</li>
))}
</ul>
);
}
面白いな と思ったところ
- フレームワークが PostsPage に Props をわたしている
loaderData で API のレスポンスが見えるなら suspense でラップするぞ!と思ったのが失敗でした。あくまで Suspense
はラップしたコンポーネントでフォールバックを起こして fallback のコンポーネントを表示するので PostsPage
コンポーネントの中でやるべきですし、正確な Props はフレームワークが知っていることなので export default
を使って外部に公開する必要があります。
/* 間違った使い方 */
<Suspense>
<PostsPage params={{}} loaderData={undefined} matches={[]} />
</Suspense>
Suspense の動作については誤った説明だと思います フォールバックの説明が難しいです。
- loaderData が同期的に API のレスポンスをわたしている
わたしの理解では loaderData を使うと非同期で行ったサーバーコンポーネントの処理( API リクエスト )を同期的に取得してコンポーネントを描画する役割があります。コンポーネントは async / await をつけて定義できないのでここは「おおー、すごいな」と思いました(なぜそういった設計かは知らない)。
今回は Suspense
を使いたいのでラップしたコンポーネントのなかでは use
を使って一旦はレンダリングをフォールバックする必要があります。そのため loader
では Promise
を返す必要があり、かつオブジェクトで返さなければなりません。オブジェクトではなく Promise
オブジェクトだと処理が終わるまで止まってしまいます
おわりに
次のアクションとしては suspense を使った無限スクロールにチャレンジしたです。コンポーネント名やベストプラクティスについては目をつむってください