React Router v7(フレームワーク利用) 実践ガイド:ブログサイトを作りながら学ぶサーバーサイド、クライアントサイドのレンダリング
はじめに
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(フレームワーク利用) 実践ガイド:ブログサイトを作りながら学ぶ最新ルーティング
React Router v7のフレームワーク機能を使ったブログサイト構築のステップバイステップガイド。トップページや動的ルーティング、セグメントの実装例を交えて、新しいルーティング機能を活用する方法を解説します。
shinagawa-web.com
React Router v7(ライブラリ利用)を使ったブログサイトの構築ガイド
React Router v7を使用して、ブログサイトを構築するための基本的なルーティング設定を紹介します。動的セグメント、個別記事の表示、ネストされたルートなどを活用して、効率的なページ遷移を実現する方法を学べます。
shinagawa-web.com
React開発を加速するVite入門:高速で柔軟なプロジェクトセットアップガイド
Viteを活用したReactプロジェクトのセットアップ方法を徹底解説!Viteの基本特徴から環境変数の使い方、便利なカスタマイズ方法まで、開発スピードを最大化するヒントをお届けします。
shinagawa-web.com
スキーマ駆動開発の実践:React × Express × GraphQLで効率的なAPI設計を実現
GraphQLを活用したスキーマ駆動開発(Schema-Driven Development, SDD)の実践方法を解説します。React(フロントエンド)とExpress(バックエンド)を組み合わせ、GraphQLスキーマを基にAPI設計を進めることで、型安全性と開発効率を向上させる手法を紹介します。GraphQL Code Generatorを用いた型定義の自動生成、スキーマ設計のベストプラクティス、Turborepoによるモノレポ構成についても詳しく解説します。
shinagawa-web.com
フロントエンドのテスト自動化戦略:Jest・Playwright・MSW を活用したユニット・E2E・API テスト最適化
フロントエンド開発において、品質を担保しながら効率的に開発を進めるためには、適切なテストの自動化が不可欠です。本記事では、Jest や Vitest を活用したユニットテストの導入・強化、React Testing Library や Storybook との統合によるコンポーネントテストの最適化、Playwright / Cypress を用いた E2E テストの拡充について詳しく解説します。さらに、Supertest や MSW を活用した API テストの自動化、Faker / GraphQL Mock によるモックデータの整理、CI/CD パイプラインにおける並列実行やキャッシュ活用による最適化など、テストを効果的に運用するための手法を紹介。また、Codecov / SonarQube によるテストカバレッジの可視化や、フィーチャーフラグを考慮したテスト戦略の策定についても解説し、実践的なアプローチを提案します。テストの信頼性と効率を向上させ、開発プロセスを強化したいフロントエンドエンジニア必見の内容です。
shinagawa-web.com
Webアクセシビリティの完全ガイド:Lighthouse / axe による自動テスト、WCAG基準策定、キーボード操作・スクリーンリーダー対応まで
Webアクセシビリティの課題を解決するための包括的なガイド。Lighthouse / axe を活用した自動テストの設定、WCAGガイドラインに基づく評価基準の整備、キーボード操作やスクリーンリーダー対応の改善、カラーコントラストの最適化、ARIAランドマークの導入、フォームやモーダルの操作性向上まで詳しく解説。定期的なアクセシビリティレポートを活用し、継続的な改善を実現する方法も紹介します。
shinagawa-web.com
チャットアプリ(画像・PDF送信、ビデオ通話機能付き)
お客様固有の要件を除き一般的なチャットアプリに求められる最低限の機能を実装しデモアプリとしてご紹介いたします。
shinagawa-web.com
React × Tailwind CSS × Emotionで実践するコンポーネント設計ガイド:デザインシステム・状態管理・再利用性の最適解
React、Tailwind CSS、Emotion、Storybook、Figma、Next.jsを活用したコンポーネント設計のベストプラクティスを紹介。デザインシステムに基づく命名規則、適切な状態管理、再利用性を高める抽象化、アクセシビリティ対応、スタイルガイドラインの整備、テーマ設定、バージョン管理、ドキュメント作成まで、モダンフロントエンド開発に欠かせない知識を徹底解説します。
shinagawa-web.com
弊社の技術支援サービス
無駄なコストを削減し、投資対効果を最大化する
クラウド費用の高騰、不要なSaaSの乱立、開発工数の増加――これらの課題に悩んでいませんか?本サービスでは、クラウドコストの最適化、開発効率向上、技術選定の最適化 を通じて、単なるコスト削減ではなく、ROIを最大化する最適解 をご提案します。
shinagawa-web.com
最新技術の導入・検証を支援するPoCサービス
Remix、React Server Components、TypeScript移行、クラウドサービス比較、マイクロサービス、サーバーレス、デザインシステムなど、最新技術のPoC(概念実証)を通じて、最適な技術選定と導入を支援します。貴社の開発課題に合わせた検証・実装で、ビジネスの成長を加速させます。
shinagawa-web.com
開発生産性を最大化するための支援サービス
開発チームの生産性向上、コードの品質管理、インフラの最適化まで、様々な側面からサポートします。コードベースのリファクタリングから、テスト自動化、オンボーディング強化まで、プロジェクトの成功に必要なすべての支援を提供。御社の開発現場が効率的に機能するように、技術的な障害を取り除き、スムーズな開発を実現します。
shinagawa-web.com
開発品質向上支援 – 効率的で安定したプロダクトを実現
フロントエンドからバックエンド、データベースまで、開発プロセス全体を最適化し、安定したプロダクト作りをサポートします。コードレビューの仕組み、型定義の強化、E2Eテスト環境の構築など、開発の各ステップにおけるベストプラクティスを導入することで、より効率的でバグの少ない、そしてユーザー満足度の高いサービス提供を支援します。
shinagawa-web.com
Webアプリのセキュリティ強化支援
Webアプリの脆弱性対策からインフラのセキュリティ強化まで、包括的なセキュリティ支援を提供。OWASP Top 10対策、JWT認証の最適化、APIのアクセス制御、依存パッケージの監査、セキュアコーディングの標準化など、実践的なアプローチで開発現場の安全性を向上させます。
shinagawa-web.com
目次
お問い合わせ