はじめに
React Router v7のReactフレームワークとしての機能を活かしてブログサイトを作りながらルーティングの機能を前回の記事でご紹介しました。
今回はその記事の続きとなります。
サーバサイド、クライアントサイドでのレンダリングなどフレームワークを最大限活用した機能のご紹介をします。
React Router とは
React Routerは、Reactアプリケーション向けのルーティングライブラリです。ルーティングとは、ユーザーが特定のURLにアクセスした際に、どのコンポーネントを表示するかを制御する仕組みのことです。
React Routerはv7よりReactフレームワークとして最大限に使用することも、独自のアーキテクチャを持つライブラリとして最小限に使用することもできます。
今回の記事のゴール
JSON Placeholderというサンプルのjson
を返すサーバーを用いて投稿データをサーバサイド、クライアントサイドで取得する方法をご紹介します。
レスポンスが遅い場合は考慮してスピナーなどを処理中に表示するなども併せて実装し解説しています。
セットアップ
React、Vite、React Router v7などの基本的なライブラリのセットアップについては前回の記事をご確認ください。
サーバーサイドレンダリング
ここではサーバーサイドレンダリングを行うための設定を行います。
と言いつつも既に設定はされております。
import type { Config } from '@react-router/dev/config'
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config
プロジェクト作成段階でssr: true
となっておりサーバーサイドレンダリングが活用できるようになっております。
JSON Placeholder
前回の記事ではブログデータをファイルに保存してアクセスしていました。
今回は少しだけ本物?らしくJSON PlaceholderというサイトからAPIでブログデータのサンプルを取得してテストを実施していきます。
トップページに記載されていますが下記のコマンドでデータを取得することができます。(ユーザー登録や認証は不要です。)
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))
レスポンス結果
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
こちらの例ではtodos
からid: 1のデータのみ取得しています。
サーバーサイドレンダリング
それではJSON Placeholderでデータ取得をサーバーサイドで行い、レンダリングまでサーバーサイドで済ませた上で画面に表示させます。
今回はトップページに投稿一覧を表示させてみようと思います。
type Post = {
userId: number;
id: number;
title: string;
body: string;
}
export const fetchPosts = async (): Promise<Post[]> => {
console.log('fetch')
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return await response.json()
}
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
JSON Placeholderから投稿一覧のデータを取得する処理となります。
type Post = {
userId: number;
id: number;
title: string;
body: string;
}
データの中身を取り出しやすいように型を定義しておきます。
なお型をどうやって定義しているかというと、実際にデータを取得して確認しました。。。
console.log('fetch')
この関数がどこで動いているかをわかりやすくするために実行時に毎回fetch
という文字を出力します。
トップページに投稿一覧を表示させるためトップページを修正します。
- import { posts } from '~/const/posts'
import type { Route } from './+types/home'
import { Link } from 'react-router'
import { HomeNavigation } from '~/components/home-navigation'
+ import { fetchPosts } from '~/lib/data'
export function meta({}: Route.MetaArgs) {
return [
{ title: 'New React Router App' },
{ name: 'description', content: 'Welcome to React Router!' },
]
}
+ export async function loader() {
+ const posts = await fetchPosts()
+ return posts
+ }
export default function Home({
+ loaderData,
+ }: Route.ComponentProps) {
+ const posts = loaderData;
return (
<>
<HomeNavigation />
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link to={`/post/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</>
)
}
ルートローダーは、ルートコンポーネントがレンダリングされる前に、ルートコンポーネントにデータを提供します。サーバーレンダリング時、サーバー上で呼び出されます。
export async function loader() {
const posts = await fetchPosts()
return posts
}
取得したデータをコンポーネントで受け取るときに使用するのがloaderData
となります。
export default function Home({
loaderData,
}: Route.ComponentProps) {
const posts = loaderData;
return (
<>
<HomeNavigation />
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link to={`/post/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</>
)
}
型を定義していますのでloader()
の戻り値がPromise<Post[]>
となっています。
その後のloaderData
はPost[]
となっていますので後はそのままレンダリングできます。
実際に画面で表示されるか確認します。
npm run dev
で起動したコンソールを確認すると、fetch
の表示があるかと思います。サーバーサイドでfetch
処理が実行されたことが確認できました。
データの取得をサーバーサイドで行うことは確認できましたがレンダリングまでサーバーサイドで実行しているかを確認してみたいと思います。
データ取得処理にタイマーをセットします。
fetchPosts
が呼び出されると3秒間待ってから実行されます。
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
export const fetchPosts = async (): Promise<Post[]> => {
+ await sleep(3000)
console.log('fetch')
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return await response.json()
}
実行してみると「トップ」をクリックしてからしばらく動きがありません。
サーバーサイドで3秒間待機し、その後データローディングとレンダリングを行い完了したらレスポンスを返してブラウザで表示されるという挙動になります。
(この動画だとクリックしたタイミングがわかりにくい。。。)
今回は意図的に3秒間待ちましたが実際の開発プロジェクトでもレスポンスの遅い処理が出てくるかと思います。
何も表示しないのはUI的によろしくないのでサーバーサイドでレンダリングしている間にスピナーを表示しようと思います。
まずはスピナーコンポーネントの作成。
export const Spinner = () => {
return (
<div
className="flex h-screen items-center justify-center"
aria-label="読み込み中"
>
<div className="h-10 w-10 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
</div>
)
}
次はデータ取得処理を修正します。
export const fetchPosts = (): Promise<Post[]> => {
console.log('fetch')
return new Promise((resolve) =>
setTimeout(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then((response) => response.json())
.then((data) => resolve(data))
}, 3000),
)
}
fetchPosts自体
はPromise
を返すだけの関数にしました。
その上でトップページのコンポーネントで実際の処理を行うよう修正します。
+ import { Suspense } from 'react'
import type { Route } from './+types/home'
- import { Link } from 'react-router'
+ import { Await, Link } from 'react-router'
import { HomeNavigation } from '~/components/home-navigation'
import { fetchPosts } from '~/lib/data'
+ import { Spinner } from '~/components/spinner'
export function meta({}: Route.MetaArgs) {
return [
{ title: 'New React Router App' },
{ name: 'description', content: 'Welcome to React Router!' },
]
}
export async function loader() {
- const posts = await fetchPosts()
+ const data = fetchPosts()
return { data }
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { data } = loaderData
return (
<>
<HomeNavigation />
+ <Suspense fallback={<Spinner />}>
+ <Await resolve={data}>
+ {(value) => {
+ return (
<ul>
+ {value.map((post) => (
<li key={post.id}>
<Link to={`/post/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
)
+ }}
+ </Await>
+ </Suspense>
</>
)
}
ルートローダーからawait
を外します。引き続きPromise
をそのまま返します。
export async function loader() {
const data = fetchPosts()
return { data }
}
そうするとloaderData
の型がPromise
になっていることがわかります。
<Suspense fallback={<Spinner />}>
<Await resolve={data}>
{(value) => {
return (
<ul>
{value.map((post) => (
<li key={post.id}>
<Link to={`/post/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
)
}}
</Await>
</Suspense>
ReactのSuspense
を使ってデータが取得できるまでの間はSpinner
コンポーネントをレンダリングします。
またdata
の実行をここで行い結果が返ってきたらレンダリングを行います。
データ取得処理が若干難しく感じるかもしれませんがこのように設定することでサーバーからのレンダリング結果が来るまでの間スピナーを表示させることができます。
クライアントサイドレンダリング(CSR)
サーバサイドで初期表示に関わるデータロードとレンダリングを行ったほうが安定したレスポンスを提供できるかと思いますが、様々な条件によりクライアントサイドでレンダリングを行うケースもあるかと思います。
下記の設定を行うことでサーバーサイドレンダリングを止めることが可能です。
既存のSPAから移行するなどで必ずCSRを行う必要がある場合は設定しておくといいかと思います。
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
} satisfies Config;
クライアントデータローディング
クライアントでJSON Placeholderからのデータをフェッチする処理を実装します。
- export async function loader() {
+ export async function clientLoader() {
const data = fetchPosts()
return { data }
}
基本的にはclientLoader
を使うことでクライアントサイドでのデータフェッチとなります。
ブラウザでコンソールログを確認するとfetch
という文字が表示されブラウザ上でフェッチされていることが確認できます。
HydrateFallback
先ほどのコンソールログにもメッセージが出ていましたがclientLoader
を使う際にはHydrateFallback
を使うことを推奨されています。
このコンポーネントはクライアントサイドでのHydrationプロセス中に代替コンポーネントを表示するための仕組みです。
具体的には、サーバーサイドでプリレンダリングされたコンテンツ(HTML)がクライアントサイドでReactに「引き継がれる」過程(Hydration)があります。このHydrationが完了するまでの間、UIの一部が動かない(インタラクティブでない)状態になる場合があります。この間にユーザーに何か視覚的なフィードバックを提供するためにHydrateFallbackを使用します。
Homeコンポーネントと並列の位置関係でHydrateFallback
を設定します。
+ export function HydrateFallback() {
+ return <div>Loading...</div>;
+ }
export default function Home({ loaderData }: Route.ComponentProps) {
const { data } = loaderData
return (
<>
<HomeNavigation />
<Suspense fallback={<Spinner />}>
<Await resolve={data}>
{(value) => {
return (
<ul>
画面をリロードするとLoading...
の文字が表示されているのがほんの一瞬ですが確認できます。
またclientLoader
についてはhydrate
プロパティを設定することもできます。
このプロパティをtrue
に設定するとハイドレーション中およびページがレンダリングされる前にクライアントローダーを強制的に実行することもできます。
+ clientLoader.hydrate = true as const
ただ今回の検証環境ではハイドレーションが非常に短い時間で行われるため設定してもパフォーマンスに大きな違いは見られません。
さいごに
React Router v7のサーバーサイドレンダリングとクライアントサイドレンダリングの実装方法について解説しました。
特に以下のポイントを押さえることで、より良いユーザー体験を提供できることがわかりました:
サーバーサイドレンダリングによる安定したレスポンス
Suspenseとスピナーを活用した読み込み中の表示
HydrateFallbackによるスムーズな画面遷移
必要に応じたクライアントサイドレンダリングの選択
React Router v7は柔軟な実装オプションを提供しており、プロジェクトの要件に応じて最適なレンダリング方式を選択できます。
今回の記事で紹介した実装パターンを参考に、みなさんのプロジェクトに最適なレンダリング方式を検討してみてください。
次回は、React Router v7のその他の機能について詳しく解説する予定です。引き続きよろしくお願いします。
関連する技術ブログ
React Router v7(フレームワーク利用) 実践ガイド:ブログサイトを作りながら学ぶ最新ルーティング
2025/01/23React Router v7(ライブラリ利用)を使ったブログサイトの構築ガイド
2025/01/20React開発を加速するVite入門:高速で柔軟なプロジェクトセットアップガイド
2025/01/17Mock Service Worker (MSW) を使ったAPIモックとテストの効率化
2023/09/25Next.jsとAuth.jsで認証機能を実装するチュートリアル
2024/09/13Next.jsでのメール認証処理の実装ガイド:アカウント登録からトークン検証まで
2024/05/10Next.jsでのメール認証処理の実装ガイド:トークン検証からログイン画面へのリダイレクト処理までの詳細解説
2024/05/13Next.jsを活用したGitHubとGoogleのOAuth認証実装完全ガイド — スムーズなユーザーログインの実現方法
2024/06/11