メインコンテンツまでスキップ

Web Application 実装ガイド

React Router v7(Framework モード)を利用した推奨アプリケーション実装パターンを紹介します。

TODO List を表示・状態を更新する機能を実装

TODO List 一覧を表示し、状態を更新する処理を実装していきます。Todo 更新後に保存ボタンを押す動作も含まれますが、本カタログでは実際にDBなどに保存するといった動作は行わず、そのような処理が行われる想定でローダーを表示するところまでの動きを実装します。

  • Welcome page

    • /
  • Todo list page

    • /todolist

カタログ AMI にはすでに実装済みの状態に入っており、本ドキュメントではどのような流れでそのように実装されたかを辿っていただければと思います。

前提条件

root.tsx, layout など基本的なパーツの実装方法は一部省略されています。ただし、本カタログの内容は React Router v7 プロジェクト新規生成時のものとほぼ変わりません。

大まかな処理の流れ

推奨構成として、以下の順番で実装していきます。プロジェクト全体の初期セットアップ後を想定しています。

対象ファイル実装内容のサマリー
src/types/todo.d.tsTodo の型を定義
src/states/IsLoading.tsデータ処理中の状態を定義
src/hooks/useTodoSaver.tsxTodoを保存する Custom Hook を実装
src/components/TodoListItem.tsxTodoListの項目を再利用可能な構成として実装
src/pages/todolist/index.pages.tsxTodoListページのUIを実装
src/routes/todolist.tsxTodoList ページの起点とサーバー側処理(loader, action, 画面のエントリーポイント)
src/layouts/index.pages.tsxアプリ全体に適用されるレイアウトを実装
src/routes.tsTodoListページのルーティングを定義

他にも、必要に応じて適宜ディレクトリやファイルを構成してください。

Todo の型を定義

src/types/todo.d.ts を作成し、以下のように型定義をしておきます。各コンポーネントでの Props 定義や元データの型宣言で利用されます。

export type Todo = {
id: string
title: string
status: 'pending' | 'inProgress' | 'completed'
isStarred: boolean
// ...
}

また、src/types/index.ts に以下のように定義しておくと、Import 分がシンプルになるため src/ 以下全ディレクトリに作成しておくと良いです。

export * from './Todo.d'

src/states/IsLoading.ts データ処理中の状態を定義

src/states/IsLoading.ts を作成し、以下のように IsLoading の状態宣言をしておきます。本カタログでは Jotai というライブラリを利用します。

import { atom } from 'jotai'

export const IsLoadingAtom = atom<boolean>(false)

Todoを保存する Custom Hook を定義

src/hooks/useTodoSaver.tsx を作成し、Todo の変更内容を保存する処理を実装します。本カタログでは実際の更新処理は行わないですが、PUT /todolist に更新された Todo を送信し、ログ出力する動作を実装します。

import { useAtom } from 'jotai'
import * as Atoms from '~/states'

export const useTodoSaver = () => {
const [isLoading, setIsLoading] = useAtom(Atoms.IsLoadingAtom)

const saveTodo = async (todo: Todo) => {
setIsLoading(true)
const saveResult = fetch('/todolist', {
method: 'PUT',
headers: { 'content-type': 'application/json', body: JSON.stringify(todo) },
})
.then((res) => res.json())
.finally(() => setIsLoading(false))

console.log('## saveResult - ', saveResult)
}

return { isLoading, saveTodo }
}

TodoListの項目を再利用可能な構成として実装

src/components/TodoListItem.tsx を作成し、Todo 一覧の項目を実装していきます。このような一覧の項目となるコンポーネントは、他の画面での再利用できるような形を想定して配置・作成します。

import type { Todo } from '~/types'

type Props = {
todo: Todo
}

export const TodoListItem: React.FC<Props> = ({ todo }) => {

// Some dynamic ui handler methods
const onClickSave = async () => {
await saveTodo(todo).finally(() => {
toast('Info', {
description: `Sucessfully saved.`,
action: {
label: 'Close',
onClick: () => {
/* Closing toast. Nothing to do here. */
},
},
})
})
}

return (
<div className="bg-white rounded-lg border border-gray-200 p-6 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-3">
<h3 className="text-lg font-medium text-gray-900 truncate">{todo.title}</h3>

{ /* 中略。カタログ AMI に全てのサンプルコードが含まれています。 */ }

</div>
</div>
</div>
</div>
)
}

TodoListページのUIを定義

src/pages/todolist/index.pages.tsx を作成し、Todo 一覧を表示させる TodoList ページの UI を実装していきます。

import { TodoListItem } from '~/components'
import type { Todo } from '~/types'

type Props = {
items: Todo[]
}

export const TodoListPage: React.FC<Props> = (props) => {
const { items } = props

return (
<>
<main className="max-w-6xl mx-auto px-4 py-8">

{ /* 中略。カタログ AMI に全てのサンプルコードが含まれています。*/ }

<div className="space-y-4">
{items.map((todo) => (
<TodoListItem key={todo.id} todo={todo} />
))}
</div>
</main>
</>
)
}

TodoList の起点・サーバー側処理を実装

src/routes/todolist.tsx を作成しまます。src/routes/ 以下には、該当画面を読み込む際のサーバー側の処理(loader, action)と、meta タグなど各種設定が可能です。サーバー側からデータを取得し、前項の Page コンポーネントに Props 等の手段でデータを渡すことが可能で、loader を別ルートで実装し、非同期で GET /api/todolist のような形で SPA と同じようなアプローチを取ることも可能です。

import type { MetaFunction } from 'react-router'
import type { Route } from './+types/todolist'

import type { Todo } from '~/types'
import { TODO_ITEMS } from '~/fixtures'
import { TodoListPage } from '~/pages'

export const meta: MetaFunction = () => {
// biome-ignore format: for better readability
return [
{ title: 'New React Router App - Todo List Page' },
{ name: 'description', content: 'Welcome to React Router!' }
]
}

export async function loader(params: Route.LoaderArgs) {
// GET リクエストの場合、こちらの loader がサーバー側で呼ばれます。
return { TODO_ITEMS }
}

export const action = async (_params: Route.ActionArgs) => {
// POST, PUT, DELETE リクエストの場合、こちらの action がサーバー側で呼ばれます。
// sleep here for 1 seconds for demo purpose
await sleep()

// Return normal API response
return new Response(JSON.stringify({ message: 'action is called' }), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
})


const TodoListRoute = ({ loaderData }: { loaderData: { TODO_ITEMS: Todo[] } }) => {
// loader から取得したデータをクライアント側コンポーネント(pages以下)に渡します。
const { TODO_ITEMS } = loaderData
return <TodoListPage items={TODO_ITEMS} />
}

export default TodoListRoute

詳細は、React Router v7 の公式ドキュメントをご確認ください。

アプリ全体に適用されるレイアウトを実装(Loader を反映)

src/layouts/default.ts を作成し、アプリ全体に適用されるレイアウトを実装します。次項の routes.ts にてどのレイアウトを定義させるかを適宜指定することができます。

import { useAtom } from 'jotai'
import { Outlet, useLocation } from 'react-router'

import { Footer, Header, Loader } from '~/components'
import { Toaster } from '~/lib/shadcn-ui'
import * as Atoms from '~/states'

export const DefaultLayout = () => {
const [isLoading] = useAtom(Atoms.IsLoadingAtom)
const { pathname } = useLocation()

return (
<div className="min-h-screen">
<Header />

{ /* 本カタログでは Loader を layout に適用させています */ }
{ /* 必要に応じて適宜相応しいコンポーネントに配置してください。 */ }
{isLoading && (
<div className="flex h-screen w-screen items-center justify-center overflow-scroll">
<Loader isLoading={isLoading} color="blue" />
</div>
)}

{!isLoading && (
<main key={pathname} className="min-h-screen">
<div className="bg-no-repeat bg-left-top">
{ /* Outlet にページが反映されます */ }
<Outlet />
</div>
</main>
)}

<Footer />
<Toaster />
</div>
)
}

export default DefaultLayout

TodoListページのルーティングを定義

最後に、画面を開いた時にどの routes ファイルにルーティングさせるかの設定を行います。ここでは、src/routes.ts を作成し、/todolist へのリクエストが src/routes/todolist.tsx に振り分けられるように設定しておきます。

import { layout, type RouteConfig, route } from '@react-router/dev/routes'

const PREFIX = './routes'

export default [
// biome-ignore format: for better readability
layout('./layouts/default.tsx', [

// Welcome page
route('/', `${PREFIX}/welcome.tsx`),

// Todo list page
route('/todolist', `${PREFIX}/todolist.tsx`),

]),
] satisfies RouteConfig

動作確認

ここまで進めてきたら、開発モードでアプリケーションを立ち上げて動作確認を行います。ローカル実行によるデバッグとテストに進んでください。