React + React Router + TypeScript: チュートリアル[01] loaderとactionでデータを作成・編集・削除する

[React Router](https://reactrouter.com)は、Reactに使われるもっとも人気の高いルーティングのライブラリです。シングルページアプリケーション(SPA)では、ひとつのページに表示されるコンポーネントを切り替えて複数のページが表現されます。それらのページにそれぞれURLを与えて遷移するのがルーティングの技術です。

そして、React Routerは「[クライアントサイドルーティング](https://reactrouter.com/en/main/start/overview#client-side-routing)」を実現します。通常のWebサーバーでは、ページ遷移のたびにドキュメント全体をリクエストして、コンテンツすべてがロードし直されなければなりません。クライアントサイドルーティングは、すでに読み込まれたJavaScriptで、必要な箇所はただちに更新します。そのうえで、新たに使うデータのみサーバーにリクエストするのです。ページを丸ごとリロードする場合と比べて、負荷がかかりません。

React Routerにはv6.4から新たなData APIが採り入れられました(「[Using v6.4 Data APIs](https://reactrouter.com/en/main/routers/picking-a-router#using-v64-data-apis)」参照)。これからは、新APIを使うことが推奨です。このチュートリアルは、新しい構文を使った公式サイトの「[Tutorial](https://reactrouter.com/en/main/start/tutorial)」の作例にもとづきます。ただし、TypeScriptを用い、モジュールの組み立てやコードは手直ししました。解説も書き改めています。

さらに、公式「Tutorial」の説明やコードはわかりやすいものの、それぞれのモジュールの記述全体や実際の動きを確かめたいとき、自分で頭から書いてみるしかありません。そこで、このチュートリアルでは、要所ごとに[StackBlitz](https://stackblitz.com/)のコードサンプルを掲げました。各モジュールのコードが開けて見られ、動作を確認しつつ、書き替えて試すこともできるでしょう。公式「Tutorial」の記事は少し長いので、本チュートリアル解説はふたつに分けることにしました。本稿はその前編[01]です。

* [React + React Router + TypeScript: チュートリアル[02] 遷移の細かな制御と検索の機能](https://qiita.com/FumioNonaka/items/5027da3d6748eac7f060)


# インストールと設定
まず、プロジェクトのひな形をつくりましょう。[Create React App](https://create-react-app.dev/)でも結構ですし、[React Router公式「Tutorial」](https://reactrouter.com/en/main/start/tutorial#setup)では[`Vite`](https://vitejs.dev/guide/)を用いています。

React Routerのローカルへのインストールそのものは、つぎのとおりです。

```terminal
npm install react-router-dom
```

けれども、チュートリアルでは、ほかにも3つのライブラリを使います。

* [localForage](https://github.com/localForage/localForage?tab=readme-ov-file#localforage): Webアプリケーションのデータを、簡単に保存したり、取り出せる。
    - [Data API](https://localforage.github.io/localForage/#data-api)
* [match-sorter](https://github.com/kentcdodds/match-sorter?tab=readme-ov-file#match-sorter): 配列要素を絞り込んで並べ替える。
* [sort-by-typescript](https://www.npmjs.com/package/sort-by-typescript): 配列要素をキーで並べ替える。

そのため、つぎのようにセットアップしてください。

```terminal
npm install react-router-dom localforage match-sorter sort-by-typescript
```

このチュートリアルでは、[StackBlitz](https://stackblitz.com/)のテンプレート[React TypeScript]を使いました(他のツールでひな形プロジェクトをつくった場合は、ファイル名などは読み替えなければなりません)。`react-router-dom`と前掲3つのライブラリは、[Dependencies]に加えてください(後掲[サンプル001](https://stackblitz.com/edit/stackblitz-starters-xvdt2v?file=src%2FApp.tsx)参照)。

アプリケーションのはじめのモジュール構成はつぎのとおりです。`src/styles.css`の定めは書き替え、`src/contacts.ts`と`src/types.ts`のふたつのファイルを加えました。それぞれ後掲サンプル001の[StackBlitz作例](https://stackblitz.com/edit/stackblitz-starters-xvdt2v?file=src%2FApp.tsx)からコードをコピーしてください。

```
src
├── App.tsx
├── contacts.ts
├── index.tsx
├── styles.css
└── types.ts
```

`src/contacts.ts`は、WebサーバーとAPIのやり取りをするデータモジュールです。チュートリアルの主題であるReact Routerには関わらないので、解説中ではほぼ触れません(先述のセットアップで加えた3つのライブラリを用いるのはこのモジュールです)。


# ルートのルーティングを加える
ルートのルーティングに加えるモジュール`src/routes/root.tsx`は、つぎのように定めましょう。`SearchForm`コンポーネントはこのあとでつくります。

```react:src/routes/root.tsx
import type { FC } from 'react';
import { SearchForm } from '../components/search-form';

export const Root: FC = () => {
	return (
		<>
			<div id="sidebar">
				<h1>React Router Contacts</h1>
				<SearchForm />
			</div>
			<div id="detail"></div>
		</>
	);
};
```

[React Router 6.4のData API](https://reactrouter.com/en/main/routers/picking-a-router#using-v64-data-apis)でブラウザルーターをつくるのが[`createBrowserRouter`](https://reactrouter.com/en/main/routers/create-browser-router)です。引数には、[`Route`](https://reactrouter.com/en/main/route/route)オブジェクトの配列([`routes`](https://reactrouter.com/en/main/routers/create-browser-router#routes))を渡してください。ルーティングのURLが[`path`](https://reactrouter.com/en/main/route/route#path)、[`element`](https://reactrouter.com/en/main/route/route#elementcomponent)は表示するコンポーネントです。戻り値のルーター(`router`)は[`<RouterProvider>`](https://reactrouter.com/en/main/routers/router-provider)コンポーネントの`router`プロパティに与えます。

```react:src/App.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { Root } from './routes/root';

const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,
	},
]);
// export const App: FC<{ name: string }> = ({ name }) => {
export const App: FC = () => {
	return (
		/* <div>
			<h1>Hello {name}!</h1>
			<p>Start editing to see some magic happen :)</p>
		</div> */
		<RouterProvider router={router} />
	);
};
```

```react:src/index.tsx
root.render(
	<StrictMode>
		{/* <App name="StackBlitz" /> */}
		<App />
	</StrictMode>
);
```

新たに定める`SearchForm`コンポーネントのモジュール`src/components/search-form.tsx`のコードはつぎのとおりです。

```react:src/components/search-form.tsx
import type { FC } from 'react';

export const SearchForm: FC = () => {
	return (
		<div>
			<form id="search-form" role="search">
				<input
					id="q"
					aria-label="Search contacts"
					placeholder="Search"
					type="search"
					name="q"
				/>
				<div id="search-spinner" aria-hidden hidden={true} />
				<div className="sr-only" aria-live="polite"></div>
			</form>
			<form method="post">
				<button type="submit">New</button>
			</form>
		</div>
	);
};
```

さらに、ルートにはもうひとつコンポーネント(`Contacts`)を加えましょう。つぎのモジュール`src/components/contacts.tsx`がそのコードです。

```react:src/components/contacts.tsx
import type { FC } from 'react';

export const Contacts: FC = () => {
	return (
		<nav>
			<ul>
				<li>
					<a href={`/contacts/1`}>Your Name</a>
				</li>
				<li>
					<a href={`/contacts/2`}>Your Friend</a>
				</li>
			</ul>
		</nav>
	);
};
```

ルートのモジュール`src/routes/root.tsx`に、コンポーネント`Contacts`を追加してください。これで、ルーティングのルート(`/`)画面にサイドバーが表示されました(図001)。StackBlitzに公開したコードが、以下のサンプル001です。

```react:src/routes/root.tsx
import { Contacts } from "../components/contacts";

export const Root: FC = () => {
	return (
		<>
			<div id="sidebar">

				<Contacts />
			</div>

		</>
	);
};
```

#### 図001■ルーティングのルート画面に表示されたサイドバー
![qiita_2401001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/caad545c-a8d7-0060-04a6-f3ce5b69f0db.png)

#### サンプル001■React + TypeScript: React Router Tutorial 01
https://stackblitz.com/edit/stackblitz-starters-xvdt2v?file=src%2FApp.tsx


# Not Foundエラーページを加える
ルーティングが正しく動かないとき、ページは見つからないことになります。いわゆる「Not Found (404)」エラーです。React Routerにはデフォルトのエラー画面が備わっています。

たとえば、サイドバーの「Your Name」(`/contacts/1`)か「Your Friend」(`/contacts/2`)をクリックしてみましょう。遷移先パスのルーティングはまだ定めていません。つくっているアプリケーションのスタイル設定など気にしていませんので、表示されるのは見苦しい「404 Not Found」エラー画面です(図002)。

#### 図002■デフォルトの404 Not Foundエラー画面
![qiita_2401001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/4ee6df25-29e4-db2c-020d-2349aea77d38.png)

React Routerは、ルーティングでつぎのようなときに起こるエラーを拾います。

* レンダリング
* データの読み込み
* データの変更

エラーを確かめるフックは[`useRouteError`](https://reactrouter.com/en/main/hooks/use-route-error)です。自作のエラーページのモジュール`src/components/error-page.tsx`をつぎのように定めましょう。ルーティングの本題に関わらないコードの中身は、かなり手を抜きました。

```react:src/components/error-page.tsx
import type { FC } from 'react';
import { useRouteError } from 'react-router-dom';

export const ErrorPage: FC = () => {
	const error = useRouteError() as { statusText?: string; message?: string };
	console.error(error);
	return (
		<div id="error-page">
			<h1>Oops!</h1>
			<p>Sorry, an unexpected error has occurred.</p>
			<p>
				<i>{error.statusText || error.message}</i>
			</p>
		</div>
	);
};
```

エラー画面のコンポーネント`ErrorPage`は、モジュール`src/App.tsx`の`router`のルート(`/`)`Route`オブジェクトに、前掲図002のエラーメッセージで示されていたとおり[`errorElement`](https://reactrouter.com/en/main/route/route#errorelementerrorboundary)として加えます。これで、Not Foundのエラーは、自作のコンポーネントで表示されるでしょう(図003)。

```react:src/App.tsx
import { ErrorPage } from './components/error-page';

const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,
		errorElement: <ErrorPage />,
	},
]);
```

#### 図003■自作のコンポーネントで表示されたNot Foundエラー
![qiita_2401001_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ede71722-73fb-933e-fa30-fd57aa4a975c.png)


# 記録情報を表示するルートのインタフェース
アプリケーションに備えるのは、面会者(コンタクト)の情報を記録して表示する機能です。サイドバーの右側、メイン画面に加える情報表示のモジュール`src/routes/contact.tsx`はつぎのように定めます。記録(`contact`)のデータは、今のところ決め打ちです。

```react:src/routes/contact.tsx
import React from 'react';
import type { FC } from 'react';
import { Form } from 'react-router-dom';
import type { ContactType } from "../types";

export const Contact: FC = () => {
	const contact: ContactType = {
		first: 'Your',
		last: 'Name',
		avatar: 'https://placekitten.com/g/200/200',
		twitter: 'your_handle',
		notes: 'Some notes',
		favorite: true,
	};
	return (
		<div id="contact">
			<div>
				<img key={contact.avatar} src={contact.avatar || undefined} />
			</div>
			<div>
				<h1>
					{contact.first || contact.last ? (
						<>
							{contact.first} {contact.last}
						</>
					) : (
						<i>No Name</i>
					)}{' '}
					<Favorite contact={contact} />
				</h1>
				{contact.twitter && (
					<p>
						<a target="_blank" href={`https://twitter.com/${contact.twitter}`}>
							{contact.twitter}
						</a>
					</p>
				)}
				{contact.notes && <p>{contact.notes}</p>}
				<div>
					<Form action="edit">
						<button type="submit">Edit</button>
					</Form>
					<Form
						method="post"
						action="destroy"
						onSubmit={(event) => {
							if (!confirm('Please confirm you want to delete this record.')) {
								event.preventDefault();
							}
						}}
					>
						<button type="submit">Delete</button>
					</Form>
				</div>
			</div>
		</div>
	);
};
const Favorite: FC<{ contact: ContactType }> = ({ contact }) => {
	// 変数値はあとで書き替えるためletで宣言
	let favorite = contact.favorite;
	return (
		<Form method="post">
			<button
				name="favorite"
				value={favorite ? 'false' : 'true'}
				aria-label={favorite ? 'Remove from favorites' : 'Add to favorites'}
			>
				{favorite ? '★' : '☆'}
			</button>
		</Form>
	);
};
```

そうしたら、情報表示のコンポーネント(`Contact`)のルートを、`src/App.tsx`モジュールの`router`に`Route`オブジェクトとしてつぎのように加えます。`path`に定めたURLの中でコロン(`:`)が頭についたセグメント(`:contactId`)は「動的セグメント」の記述です。`/contacts/1`に遷移すれば、`:contactId`は`1`と合致してルーティングされ、情報表示画面(`Contact`コンポーネント)が開きます(図004)。変数のように動的に変化するわけです。

```react:src/App.tsx
import { Contact } from './routes/contact';

const router = createBrowserRouter([

	{
		path: 'contacts/:contactId',
		element: <Contact />,
	},
]);
```

#### 図004■動的セグメントに合致してルーティングされた情報表示画面
![qiita_2401001_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/092c0891-4861-4fd5-53fc-45551f732845.png)

もっとも、コンポーネントは`Root`が`Contact`に差し替わっています。そのため、サイドバーは消えてしまいました。サイドバーを残すには、`Contact`は`Root`の入れ子にしなければなりません。そのとき、`router`に定めた`Route`オブジェクトに用いるのが[`children`](https://reactrouter.com/en/main/routers/create-browser-router#routes)プロパティです。入れ子オブジェクトを配列に収めて与えてください。

```react:src/App.tsx
const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,
		errorElement: <ErrorPage />,
		children: [
			{
				path: 'contacts/:contactId',
				element: <Contact />,
			},
		],
	},
	/* {
		path: 'contacts/:contactId',
		element: <Contact />,
	}, */
]);
```

もうひとつ、入れ子ルートをどこにレンダーするのか、親コンポーネント(`Root`)のJSXに加えなければなりません。子ルートを示すのは[`<Outlet>`](https://reactrouter.com/en/main/components/outlet)です。こうして、親ルートのサイドバーは残しながら、子ルートのURLに遷移して、情報表示画面が開けるようになりました(図005)。

```react:src/routes/root.tsx
import { Outlet } from 'react-router-dom';

export const Root: FC = () => {
	return (
		<>

			<div id="detail">
				<Outlet />
			</div>
		</>
	);
};
```

#### 図005■親のサイドバーは残しながら子ルートに遷移して記録情報を表示する
![qiita_2401001_005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/5211bc68-056e-93fa-fd33-85f7be4d3443.png)

サイドバーのリンクは`<a>`要素で加えました(`src/components/contacts.tsx`)。すると、クリックしたとき、遷移するURLのドキュメントをそっくりブラウザにリクエストします。ブラウザの[デベロッパーツール]から開く[ネットワーク]タブで、リクエストの中身を確かめてみてください。

クライアントサイドルーティングを用いれば、アプリケーションはサーバーに別URLのドキュメント丸ごとはリクエストしません。そのとき使うのが、[`<Link>`](https://reactrouter.com/en/main/components/link)コンポーネントです。遷移先URLは`to`プロパティに与えます。モジュール`src/components/contacts.tsx`のJSXで`<a>`要素を`<Link>`コンポーネントに差し替えましょう(サンプル002)。ユーザーインタフェースがただちにレンダーされ、[デベロッパーツール]の[ネットワーク]でドキュメント全体のリクエストはなくなったことがわかるはずです。

```react:src/components/contacts.tsx
import { Link } from 'react-router-dom';

export const Contacts: FC = () => {
	return (
		<nav>
			<ul>
				<li>
					{/* <a href={`/contacts/1`}>Your Name</a> */}
					<Link to={`/contacts/1`}>Your Name</Link>
				</li>
				<li>
					{/* <a href={`/contacts/2`}>Your Friend</a> */}
					<Link to={`/contacts/2`}>Your Friend</Link>
				</li>
			</ul>
		</nav>
	);
};
```

#### サンプル002■React + TypeScript: React Router Tutorial 02
https://stackblitz.com/edit/stackblitz-starters-ar4nvs?file=src%2Fcomponents%2Fcontacts.tsx


# データを読み込む
URLセグメントやレイアウト、およびデータは複数のルートの間で互いに連携します。このアプリケーションでは、つぎのふたつのルートです。

| URLセグメント | コンポーネント | データ |
|:--|:--|:--|
| `/` | `<Root>` | 記録情報のリスト |
| `contacts/:contactId` | `<Contact>` | 個別の記録情報 |

ふたつのルートをデータの読み込みで連携するためには、`loader`と`useLoaderData`を用いて、3つの手順にしたがいます。

* ルートに`loader`関数を定める。
* `router`の`Root`オブジェクトに関数を`loader`プロパティとして加える。
* `useLoaderData`でデータを参照してレンダーする。

### ルートに`loader`関数を定める
`loader`関数はデータを読み込んで使うルートのモジュール(`src/routes/root.tsx`)に定めて`export`しましょう。

```react:src/routes/root.tsx
import type { LoaderFunction } from "react-router-dom";
import { getContacts } from "../contacts";

export const loader: LoaderFunction = async () => {
	const contacts = await getContacts(undefined);
	return { contacts };
};
```

### `router`の`Root`オブジェクトに`loader`プロパティを加える
`loader`関数は`import`して、`router`の`Route`オブジェクトに[`loader`](https://reactrouter.com/en/main/route/loader)プロパティとして定めなければなりません。

```react:src/App.tsx
// import { Root } from './routes/root';
import { Root, loader as rootLoader } from './routes/root';

const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,
		errorElement: <ErrorPage />,
		loader: rootLoader,
		children: [

		],
	},
]);
```

### `useLoaderData`でデータを参照してレンダーする
`loader`関数が読み込んだデータは、[`useLoaderData`](https://reactrouter.com/en/main/hooks/use-loader-data)フックで取り出せます。なお、`loader`関数が定められた子コンポーネント(`src/components/contacts.tsx`)からもフックによるデータの参照は可能です。

```react:src/components/contacts.tsx
// import { Link } from 'react-router-dom';
import { Link, useLoaderData } from 'react-router-dom';
import { ContactType } from '../types';

export const Contacts: FC = () => {
	const { contacts } = useLoaderData() as { contacts: ContactType[] };
	return (
		<nav>
			{/* <ul>
				<li>
					<Link to={`/contacts/1`}>Your Name</Link>
				</li>
				<li>
					<Link to={`/contacts/2`}>Your Friend</Link>
				</li>
			</ul> */}
			{contacts.length ? (
				<ul>
					{contacts.map(({ id, first, last, favorite }) => (
						<li key={id}>
							<Link to={`contacts/${id}`}>
								{first || last ? (
									<>
										{first} {last}
									</>
								) : (
									<i>No Name</i>
								)}{' '}
								{favorite && <span>★</span>}
							</Link>
						</li>
					))}
				</ul>
			) : (
				<p>
					<i>No contacts</i>
				</p>
			)}
		</nav>
	);
};
```

これで、React Routerが`loader`により読み込んだデータは、ルートとコンポーネントに連携されます。もっとも、今はロードすべき記録のデータがないので、情報のリストは空です。


# HTMLフォームでデータを送る
HTMLフォームは、`<a>`要素のリンクと同じく、ブラウザでURLを遷移します。さらに、リクエストのメソッド(`GET`または`POST`)と`POST`の場合ボディを変更できることが、リンクとの違いです。標準のHTMLによりフォームのデータは、POSTではリクエストボディ、GETなら[`URLSearchParams`](https://developer.mozilla.org/docs/Web/API/URLSearchParams)としてサーバーに送られます。React Routerがクライアントサイドルーティングを用いてデータを送る先は、ルート[`action`](https://reactrouter.com/en/main/route/action#action)です。

まだ、フォームの送信にクライアントサイドルーティングは使っていません。そのため、サイドバー上部にある新規記録作成の[New]ボタンを押すと、エラーが示されます(図006)。

```react:src/components/search-form.tsx
export const SearchForm: FC = () => {
	return (
		<div>

			<form method="post">
				<button type="submit">New</button>
			</form>
		</div>
	);
};
```

#### 図006■POSTリクエストがサーバーに送れないことを示すエラー
![qiita_2401001_006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/684a46c4-dfc1-7155-0f8f-f2f7bf2feab3.png)


# 新規の記録をつくる
クライアントサイドルーティングで新規の記録をつくる手順はつぎの3つです。

* ルートに`action`関数を定める。
* `<form>`要素を`<Form>`コンポーネントに差し替える。
* `router`の`Root`オブジェクトに`action`プロパティを加える。

### ルートに`action`関数を定める
まず、`src/routes/root.tsx`モジュールに`action`関数を定めて、`export`しましょう。

```react:src/routes/root.tsx
// import type { LoaderFunction } from "react-router-dom";
import type { ActionFunction, LoaderFunction } from "react-router-dom";

// import { getContacts } from '../contacts';
import { createContact, getContacts } from '../contacts';

export const action: ActionFunction = async () => {
	const contact = await createContact();
	return { contact };
};
```

### `<form>`要素を`<Form>`コンポーネントに差し替える
つぎに、`src/components/search-form.tsx`モジュールの`<form>`要素をReact Routerの[`<Form>`](https://reactrouter.com/en/main/components/form)コンポーネントに差し替えてください。

```react:src/components/search-form.tsx
import { Form } from 'react-router-dom';

export const SearchForm: FC = () => {
	return (
		<div>

			{/* <form method="post"> */}
			<Form method="post">
				<button type="submit">New</button>
			{/* </form> */}
			</Form>
		</div>
	);
};
```

### `router`の`Root`オブジェクトに`action`プロパティを加える
そのうえで、`src/App.tsx`モジュールに`action`関数を`import`して、`router`の`Roote`オブジェクトに`action`プロパティとして加えなければなりません。

```react:src/App.tsx
import {

	action as rootAction,
} from './routes/root';

const router = createBrowserRouter([
	{
		path: "/",
		element: <Root />,
		errorElement: <ErrorPage />,
		loader: rootLoader,
		action: rootAction,
		children: [

		],
	},
]);
```

なお、`RouteObject`の`loader`や`action`をはじめとするプロパティの型は「[Type declaration](https://reactrouter.com/en/main/route/route#type-declaration)」で確かめられます。

これで、サイドバー上部の[New]ボタンで新たな記録が作られ、デフォルトの名前「No Name」が加わるでしょう(図007)。ただし、まだ記録を削除する処理は書いていません。[New]ボタンを連打すると、「No Name」が並んだ見苦しい表示になるので、お気をつけください。

#### 図007■[New]ボタンで新たな記録が加えられる
![qiita_2401001_007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/b1d70df3-edd9-f47e-e583-fd2272cb68d8.png)

もっとも、サイドバーに示された名前(「No Name」)をクリックしても、右側には`Contact`コンポーネント(`src/routes/contact.tsx`)に決め打ちした情報(`contact`)が表れるだけです。それでも、URLの`:contactId`には、新規作成されたIDが挿入されていることにご注目ください。

ところで、サイドバー上部の[New]ボタン(`src/components/search-form.tsx`)には、`type="submit"`が定められているだけです。記録のリスト(`src/components/contacts.tsx`)についても、データを状態に収めているわけではありません。記録のデータはどのように更新されたのでしょう。

```react:src/components/search-form.tsx
export const SearchForm: FC = () => {

	return (
		<div>

			<Form method="post">
				<button type="submit">New</button>
			</Form>
		</div>
	);
};
```

`<Form>`コンポーネントは`submit`でリクエストをサーバーでなく、ルートの`action`に送ります。そして、POSTが意味するのは、何らかのデータを変更するということです。そのため、React Routerは、`action`が終わると同時にページのデータを再検証します。すると、`useLoaderData`フックによる更新が実行されて、ユーザーインタフェースはデータと同期するのです。


# `loader`関数の引数で`params`オブジェクトを受け取る
新規の記録に固有の`:contactId`が定められました。その動的なセグメントには、ID以外にもそれぞれ違った値が与えられます。これらが、URLパラメータ(`params`オブジェクト)です。

URLパラメータは`loader`関数の引数に、プロパティ[`params`](https://reactrouter.com/en/main/route/loader#params)として渡されます。そのとき、キーとなるのが動的セグメントです。このアプリケーションで面会記録のセグメントは`:contactId`としました。すると、キーは`params.contactId`として取り出せます。これが、記録のリストからデータを探して特定するとき役立つのです。

`loader`関数を加えるのは、モジュール`src/routes/contact.tsx`です。`params`から得た`contactId`を、データモジュール`src/contacts.ts`の関数`getContact`に渡すと表示すべき記録のデータ(`contact`)が返ります。コンポーネント`Contact`でデータを取り出すのが`useLoaderData`です。

```react:src/routes/contact.tsx
// import { Form } from 'react-router-dom';
import { Form, useLoaderData } from 'react-router-dom';
import type { LoaderFunction } from 'react-router-dom';
import { getContact } from '../contacts';
// import type { ContactType } from '../types';
import type { ContactType, Params } from '../types';

export const loader: LoaderFunction<Params> = async ({
	params: { contactId },
}) => {
	const contact = await getContact(contactId);
	return { contact };
};
export const Contact: FC = () => {
	/* const contact: ContactType = {
		first: 'Your',
		last: 'Name',
		avatar: 'https://placekitten.com/g/200/200',
		twitter: 'your_handle',
		notes: 'Some notes',
		favorite: true,
	}; */
	const { contact } = useLoaderData() as { contact: ContactType };

};
```

`loader`関数は、モジュール`src/App.tsx`で、`router`の子`Route`オブジェクトに`loader`プロパティとして定めなければなりません。

```react:src/App.tsx
// import { Contact } from "./routes/contact";
import { Contact, loader as contactLoader } from "./routes/contact";

const router = createBrowserRouter([
	{
		path: "/",

		children: [
			{
				path: "contacts/:contactId",
				element: <Contact />,
				loader: contactLoader,
			},
		],
	},
]);
```

これで、決め打ちしたデータでなく、動的セグメント(`:contactId`)に応じた情報がサイドバーの右に表示されます(図008)。もっとも、動的セグメントのキー以外、データは実質的に空です。

#### 図008■動的セグメントに一致した情報がサイドバーの右に表示される
![qiita_2401001_008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/786e2037-2da1-2c70-0456-ef675d221685.png)


# データを更新する
そこで、新規記録のデータを書き替えられるようにします。

### 新規記録データの編集画面をつくる
新規記録データの編集画面をつくって、情報が書き加えられるようにしましょう。新たな記録編集のモジュールは、`src/routes/edit.tsx`です。表示する情報は、`useLoaderData`で取り出します。

```react:src/routes/edit.tsx
import type { FC } from 'react';
import { Form, useLoaderData } from 'react-router-dom';
import type { ContactType } from '../types';

export const EditContact: FC = () => {
	const {
		contact: { first, last, twitter, avatar, notes },
	} = useLoaderData() as { contact: ContactType };
	return (
		<Form method="post" id="contact-form">
			<p>
				<span>Name</span>
				<input
					placeholder="First"
					aria-label="First name"
					type="text"
					name="first"
					defaultValue={first}
				/>
				<input
					placeholder="Last"
					aria-label="Last name"
					type="text"
					name="last"
					defaultValue={last}
				/>
			</p>
			<label>
				<span>Twitter</span>
				<input
					type="text"
					name="twitter"
					placeholder="@jack"
					defaultValue={twitter}
				/>
			</label>
			<label>
				<span>Avatar URL</span>
				<input
					placeholder="https://example.com/avatar.jpg"
					aria-label="Avatar URL"
					type="text"
					name="avatar"
					defaultValue={avatar}
				/>
			</label>
			<label>
				<span>Notes</span>
				<textarea name="notes" defaultValue={notes} rows={6} />
			</label>
			<p>
				<button type="submit">Save</button>
				<button type="button">Cancel</button>
			</p>
		</Form>
	);
};
```

`loader`関数は、面会記録表示のモジュール`src/routes/contact`から使い回すことにしました(実際の開発では、ルートごとに別に定めるのが通常です)。モジュール`src/App.tsx`の`router`に`Contact`コンポーネントと同じ`children`として加え、子`Route`オブジェクトの`loader`プロパティに定めましょう。

```react:src/App.tsx
import { EditContact } from './routes/edit';

const router = createBrowserRouter([
	{
		path: '/',

		children: [

			{
				path: 'contacts/:contactId/edit',
				element: <EditContact />,
				loader: contactLoader,
			},
		],
	},
]);
```

記録表示の画面で[Edit]ボタンをクリックすると、`EditContact`コンポーネントの編集画面に遷移します(図009)。これは、記録表示の`Contact`コンポーネントで、ボタン(`<button type="submit">`)が含まれる`<Form>`コンポーネントに、[`action`](https://reactrouter.com/en/main/components/form#action)プロパティとして遷移先のセグメント`edit`が定められていたからです(`contacts/:contactId/edit`)。

プロパティの働きは、標準HTMLにおける[`<form>`](https://developer.mozilla.org/docs/Web/HTML/Element/form)要素の`action`属性と基本的に変わりません。ただし、デフォルトのURLは相対パスとなることにご注意ください。

```react:src/routes/contact.tsx
export const Contact: FC = () => {

	return (
		<div id="contact">

			<div>

				<div>
					<Form action="edit">
						<button type="submit">Edit</button>
					</Form>

				</div>
			</div>
		</div>
	);
};
```

#### 図009■記録編集の画面が表示される
![qiita_2401001_009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/aa9a25b7-bdf8-58e2-8fb1-24450ded23a2.png)

### `FormData`で記録のデータを更新する
記録更新のためには、書き替えたデータをルートの`action`に送らなければなりません。モジュール`src/routes/edit.tsx`に、`action`関数をつぎのように加えましょう。データモジュール`src/contacts.ts`から`import`した`updateContact`に、`contactId`と更新値の`FormData`オブジェクト`updates`を渡してデータが書き替わります。

```react:src/routes/edit.tsx
// import { Form, useLoaderData } from 'react-router-dom';
import { Form, useLoaderData, redirect } from 'react-router-dom';
import type { ActionFunction } from 'react-router-dom';
import { updateContact } from '../contacts';
// import type { ContactType } from '../types';
import type { ContactType, Params } from '../types';

export const action: ActionFunction<{
	request: Request;
	params: Params;
}> = async ({ request, params: { contactId } }) => {
	const formData = await request.formData();
	const updates = Object.fromEntries(formData);
	await updateContact(contactId, updates);
	return redirect(`/contacts/${contactId}`);
};
```

環境によっては、TypeScriptのつぎのような警告が示されるかもしれません。メッセージのとおり、[`Object.fromEntries()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)は[ECMAScript 2019](https://qiita.com/tonkotsuboy_com/items/07f8ef98abf89250b90c#%E3%82%AD%E3%83%BC%E5%80%A4%E3%81%AE%E3%83%9A%E3%82%A2%E3%81%8B%E3%82%89%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E7%94%9F%E6%88%90---objectfromentries)からの仕様です。

> Property 'fromEntries' does not exist on type 'ObjectConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2019' or later.

この場合、`tsconfig.json`の`compilerOptions`で、`lib`の対応を`es2019`に書き替えなければなりません。

```json:tsconfig.json
{

	"compilerOptions": {

		"lib": [
			"dom",
			// "es2015"
			"es2019"
		],

	}
}
```

`action`関数はモジュール`src/App.tsx`の`router`で、`children`の子`Route`オブジェクトに`action`として定めてください。

```react:src/App.tsx
// import { EditContact } from './routes/edit';
import { action as editAction, EditContact } from './routes/edit';

const router = createBrowserRouter([
	{
		path: '/',

		children: [

			{
				path: 'contacts/:contactId/edit',
				element: <EditContact />,
				loader: contactLoader,
				action: editAction,
			},
		],
	},
]);
```

[Save]ボタンは`EditContact`コンポーネントの`<Form method="post">`に、`<button type="submit">`要素で加えられていました。したがって、POSTリクエストは`action`に送られて、データが更新されるのです(図010)。各モジュールのコードと動きは、以下のサンプル003でお確かめください。

```react:src/routes/edit.tsx
export const EditContact: FC = () => {

	return (
		<Form method="post" id="contact-form">

			<p>
				<button type="submit">Save</button>

			</p>
		</Form>
	);
};
```

#### 図010■記録のデータが更新された
![qiita_2401001_010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/4aaa65ed-0fd8-f0f3-c074-5097e551b274.png)

#### サンプル003■React + TypeScript: React Router Tutorial 03
https://stackblitz.com/edit/stackblitz-starters-fzevla?file=src%2FApp.tsx

### データはどのように更新されるか
モジュール`src/routes/edit.tsx`の`action`関数で、データがどのように更新されるか、少し詳しく見ていきましょう。

標準HTMLでフォーム(`<form>`)を送信(`submit`)すると、[`FormData`](https://developer.mozilla.org/docs/Web/API/FormData)がつくられます。そして、リクエストのボディとしてサーバーに送られるのです。React Routerではサーバーではなく、`action`関数の引数オブジェクトに`request`プロパティで渡されます。[`Request`](https://developer.mozilla.org/docs/Web/API/Request)オブジェクトから`FormData`オブジェクト(`Promise`)を得て返すのが、[`request.formData`](https://developer.mozilla.org/docs/Web/API/Request/formData)メソッドです。

```react:src/routes/edit.tsx
export const action: ActionFunction<{
	request: Request;
	params: Params;
}> = async ({ request, params: { contactId } }) => {
	const formData = await request.formData();

};
```

さて、`EditContact`コンポーネントの`<input type="text">`要素には`name`属性が与えられていました。

```react:src/routes/edit.tsx
export const EditContact: FC = () => {
	const {
		contact: { first, last, twitter, avatar, notes },
	} = useLoaderData() as { contact: ContactType };
	return (
		<Form method="post" id="contact-form">
			<p>
				<span>Name</span>
				<input

					type="text"
					name="first"

				/>
				<input

					type="text"
					name="last"

				/>
			</p>
   
		</Form>
	);
};
```

すると、[`FormData.get()`](https://developer.mozilla.org/docs/Web/API/FormData/get)メソッドを用いて、`formData.get(name)`の構文でそれぞれの値が得られます。もっとも、フィールドが多くなると、変数の数も増えてしまって煩雑です。

```react
const formData = await request.formData();
const firstName = formData.get('first');
const lastName = formData.get('last');
console.log(firstName, lastName);
```

そこで、[`Object.fromEntries()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)メソッドを使いました。返されるのは、キーと値の組みをプロパティとしていくつでももてるひとつのオブジェクト(`updates`)です。ここでご紹介したAPIやメソッドは、React Router独自の実装ではなく、Webプラットフォームの仕様であることにご注目ください。

```react:src/routes/edit.tsx
export const action: ActionFunction<{
	request: Request;
	params: Params;
}> = async ({ request, params: { contactId } }) => {
	const formData = await request.formData();
	const updates = Object.fromEntries(formData);

};
```

この`updates`オブジェクトを`contactId`とともに関数`updateContact`に渡せば、記録データは更新されます。

```react:src/routes/edit.tsx
await updateContact(contactId, updates);
```

モジュール`src/routes/edit.tsx`の`action`関数が返すのは、[`redirect`](https://reactrouter.com/en/main/fetch/redirect)の戻り値で、[`Response`](https://developer.mozilla.org/docs/Web/API/Response)オブジェクトです。関数はリクエスト(`request`)を受け取るので、レスポンスが返されるのは自然でしょう。

引数のパスにページを遷移するのが`redirect`です。クライアントルーティングでなければ、POSTリスエストのあとサーバーがページを移すと、新たなデータで描き替わります。この振る舞いは、React Routerでも変わりません。`action`が終わると、ページのデータは再検証されるのです。こうして、フォームデータの[Save]\(`submit`)により、サイドバーの情報が更新されました。

```react:src/routes/edit.tsx
export const action: ActionFunction<{
	request: Request;
	params: Params;
}> = async ({ request, params: { contactId } }) => {

	return redirect(`/contacts/${contactId}`);
};
```


### 新規記録を作成したら編集ページに遷移する
`redirect`でページは遷移できるのですから、[New]ボタンで新規記録をつくったとき、データが空っぽのまま済ませるのはいただけません。自動的にデータ編集画面(`/contacts/:contactId/edit`)に移るべきでしょう。

```react:src/routes/root.tsx
// import { Outlet } from 'react-router-dom';
import { Outlet, redirect } from 'react-router-dom';

export const action: ActionFunction = async () => {

	// return { contact };
	return redirect(`/contacts/${contact.id}/edit`);
};
```


# 選ばれているリンクのスタイルを変える
今のままでは、サイドバーのリストからどの記録を選んで(アクティブ)、右側の情報が表示されているのかわかりません。リンクがアクティブかどうか調べられるのが[`<NavLink>`](https://reactrouter.com/en/main/components/nav-link)コンポーネントです。`<Link>`と差し替えれば、`isActive`(ブール値)でアクティブかどうかが調べられます。

つぎのコードは、アクティブなリンクのスタイルをCSSのクラス(`className`)で切り替えました(図011)。なお、`isPending`(ブール値)は、アクティブになるまでの経過状態です。遷移に待ちが生じる場合の扱いに役立つでしょう。

```react:src/components/contacts.tsx
// import { Link, useLoaderData } from 'react-router-dom';
import { Link, NavLink, useLoaderData } from 'react-router-dom';

export const Contacts: FC = () => {

	return (
		<nav>
			{contacts.length ? (
				<ul>
					{contacts.map(({ id, first, last, favorite }) => (
						<li key={id}>
							{/* <Link to={`contacts/${id}`}> */}
							<NavLink
								to={`contacts/${id}`}
								className={({ isActive, isPending }) =>
									isActive ? 'active' : isPending ? 'pending' : ''
								}
							>

							{/* </Link> */}
							</NavLink>
						</li>
					))}
				</ul>
			) : (

			)}
		</nav>
	);
};
```

#### 図011■アクティブなリンクのスタイルをCSSで変更した
![qiita_2401001_011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/10a476c5-c88c-9c87-5db2-1c16769d2d05.png)


# グローバルな読み込み待ちのユーザーインタフェース
React Routerでユーザーの操作によりアプリケーションのページを遷移したとき、つぎのページのデータが読み込まれるまで、前のページは表示されたままになります。すると、アプリケーションが反応していないとユーザーに感じさせるかもしれません。ページが読み込み中であることをユーザーに知らせるとよいでしょう。

React Routerは、動的なアプリケーションがつくれるよう、背後の状態を管理しています。今回使うのは、ページナビゲーションの情報を調べる[`useNavigation`](https://reactrouter.com/en/main/hooks/use-navigation)フックです。

```react
const navigation = useNavigation();
```

[`navigation.state`](https://reactrouter.com/en/main/hooks/use-navigation#navigationstate)は、現在のナビゲーションの状態をつぎの3つの文字列の値いずれかで示します。

* `'idle'`: 読み込み待ちはない。
* `'submitting'`: ルートの`action`が呼び出され、POSTなどでフォーム送信している。
* `'loading'`: つぎのルートの`loader`が呼び出され、遷移先ページをレンダーしている。

モジュール`src/routes/root.tsx`でサイドバー右側の`<div>`要素(`id="detail"`)に、`state`が`'loading'`の場合のCSSクラス(`className`)を与えました。アニメーションでフェードアウトするクラス(`loading`)です。これで、画面が読み込み中であることは伝わるでしょう(後掲サンプル004参照)。

```react:src/routes/root.tsx
// import { Outlet, redirect } from 'react-router-dom';
import { Outlet, redirect, useNavigation } from 'react-router-dom';

export const Root: FC = () => {
	const { state } = useNavigation();
	return (
		<>

			{/* <div id="detail"> */}
			<div id="detail" className={state === 'loading' ? 'loading' : ''}>
				<Outlet />
			</div>
		</>
	);
};
```

WebサーバーとAPIのやり取りをするデータモジュール`src/contacts.ts`には、クライアントサイドキャッシュが備わっていることにご注意ください。そのため、すでに開いた記録の情報に遷移すれば、読み込み待ちもなくただちに切り替わります。再び試すときは、アプリケーションをロードし直し、場合によってはキャッシュも削除してください。

これは、React Routerの振る舞いではありません。ルートを変えれば、再度の遷移かどうかにかかわらず、データは読み込み直されます。ただし、サイドバーのようにナビゲーションしてもそのまま残るルートは、ローダーが呼び出されません。


# 記録を削除する
ようやく加えるのが、記録を削除する機能です。

### 記録の[Delete]ボタンは`Contact`コンポーネントに備わっている
記録を削除する[Delete]ボタン(`<button type="submit">`)は、すでに`src/routes/contact.tsx`モジュールの`Contact`コンポーネントに備わっています。そして、親の`<Form>`コンポーネントに添えられた[`action`](https://reactrouter.com/en/main/components/form#action)プロパティの値は、`"destroy"`です(親ルート`contact/:contactId`に対する相対パス)。したがって、[`confirm`](https://developer.mozilla.org/docs/Web/API/Window/confirm)のダイアログで[OK]をクリックすれば、`contact/:contactId/destroy`に遷移することになります。

```react:src/routes/contact.tsx
export const Contact: FC = () => {

	return (
		<div id="contact">
	
		<div>
	
			<div>
	
				<Form
					method="post"
					action="destroy"
					onSubmit={(event) => {
						if (!confirm('Please confirm you want to delete this record.')) {
							event.preventDefault();
						}
					}}
				>
					<button type="submit">Delete</button>
				</Form>
			</div>
		</div>
		</div>
	);
};
```

もっとも、ルート`contact/:contactId/destroy`も、遷移先のコンポーネントもまだつくっていません(ページが見つからないというエラーになります)。

### 新たな`destroy`のモジュールに`action`関数を定める
新しくつくるモジュール`src/routes/destroy.tsx`に、`action`関数を定めましょう。関数内で呼び出す`deleteContact`はデータモジュール`src/contacts.ts`から`import`しました。引数は、削除すべき記録を特定する`contactId`です。

```react:src/routes/destroy.tsx
import { ActionFunction, redirect } from 'react-router-dom';
import { deleteContact } from '../contacts';
import { Params } from '../types';

export const action: ActionFunction<Params> = async ({
	params: { contactId },
}) => {
	if (contactId) {
		await deleteContact(contactId);
	}
	return redirect('/');
};
```

### `router`に与える`Route`オブジェクト配列に`destroy`ルートを加える
あとは、モジュール`src/App.tsx`の`router`で、パス(`path`)`contacts/:contactId/destroy`の`Route`オブジェクトをルート(`/`)パスの子(`children`)として加えればよいでしょう。なお、前掲`src/routes/destroy.tsx`モジュールの`action`関数は、`redirect`によるルートパス(`/`)への遷移を返しました。そのため、表示するコンポーネントの`element`プロパティは定めません。

```react:src/App.tsx
import { action as destroyAction } from './routes/destroy';

const router = createBrowserRouter([
	{
		path: '/',

		children: [

			{
				path: 'contacts/:contactId/destroy',
				action: destroyAction,
			},
		],
	},
]);
```

これで、`Contact`コンポーネントの編集画面で[Delete]ボタンをクリックすれば、記録はリストから削除されるはずです(サンプル004)。

#### サンプル004■React + TypeScript: React Router Tutorial 04
https://stackblitz.com/edit/stackblitz-starters-s5e5fa?file=src%2FApp.tsxhttps://stackblitz.com/edit/stackblitz-starters-s5e5fa?file=src%2FApp.tsx

### ページのデータはどのように削除・更新されるか
[Delete]ボタンのクリックにより、モジュール`src/routes/destroy.tsx`の`action`関数がどう呼び出され、データはどのように削除・更新されるのか、改めて確かめましょう。

1. `<Form>`コンポーネントは新しいPOSTリクエストをサーバーには送りません。ブラウザの動き気にしたがいつつ、POSTリクエストはクライアントサイドルーティングでつくられます。
2. `<Form action="destroy">`が合致するルートは新しい`contacts/:contactId/destroy`です。リクエストはこのルートに送られます。
3. そして呼び出される`action`関数が担うのは、データの削除(`deleteContact`)とルートパス(`/`)への遷移(`redirect`)です。すると、React Routerはページ上のすべてのデータの`loader`を呼び出し、最新の値を取得します(いわゆる「再検証」)。`useLoaderData`の返す新しい値により、コンポーネントは更新されるのです。

新たな`<Form>`コンポーネントにルートを定め、`action`関数さえ加えれば、あとはReact Routerがやってくれました。

***

さて、ここまでがReact Router公式「Tutorial」の中身6割方の解説です。`<Form>`コンポーネントに`loader`と`action`を組み合わせてクライアントサイドルーティングが実現されました。後半は「React + React Router + TypeScript: チュートリアル[02]」として公開予定です。