React + React Router + TypeScript: チュートリアル[02] 遷移の細かな制御と検索の機能

React Routerのv6.4から採り入れらた新しいData API(「[Using v6.4 Data APIs](https://reactrouter.com/en/main/routers/picking-a-router#using-v64-data-apis)」参照)を使った公式サイト「[Tutorial](https://reactrouter.com/en/main/start/tutorial)」の作例にもとづくチュートリアルの後編です。TypeScriptを用い、モジュールの組み立てやコードは手直ししました。解説も書き改めています。さらに、要所ごとに[StackBlitz](https://stackblitz.com/)のコードサンプルを掲げました。各モジュールのコードが開けて見られ、動作を確認しつつ、書き替えて試すこともできるでしょう。

* [React + React Router + TypeScript: チュートリアル[01] loaderとactionでデータを作成・編集・削除する](https://qiita.com/FumioNonaka/items/ffa6fbd25fe3d7a40088)

# コンテキストに応じたエラー
遷移の文脈に応じたエラー画面は、ユーザーに役立つことが少なくありません。たとえば、モジュール`src/routes/destroy.tsx`の`action`で、あえてエラーを投げてみましょう。記録の編集画面で[Delete]ボタンを押して[OK]をクリックすると、エラーが示されます(図001)。

```react:src/routes/destroy.tsx
	params: { contactId },
export const action: ActionFunction<Params> = async ({
}) => {
	throw new Error('oh dang!');

};
```

#### 図001■編集画面で[Delete]を実行するとエラーが示される
![qiita_2402001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/9d9c98e9-e1f7-9c46-1cdf-ce8cca1d0401.png)

この画面は、前回[ルートパス(`/`)の`Route`オブジェクトに`errorElement`として与えた](https://qiita.com/FumioNonaka/items/ffa6fbd25fe3d7a40088#not-found%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E5%8A%A0%E3%81%88%E3%82%8B)`ErrorPage`コンポーネントです。これで、エラーが起きたことはわかります。けれど、ユーザーはもとの画面をロードし直すことくらいしかできません。

コンテキストに応じたエラー画面を、`contacts/:contactId/destroy`パスに加えましょう。子`Route`オブジェクトでも、定めるプロパティは`errorElement`です。編集画面で[Delete]を実行すると、表示されるのは新たなエラー画面に変わりました(図002)。

```react:src/App.tsx
const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,

		children: [

			{
				path: 'contacts/:contactId/destroy',
				action: destroyAction,
				errorElement: <div>Oops! There was an error.</div>,
			},
		],
	},
]);
```

#### 図002■コンテキストに応じたエラー画面が表示される
![qiita_2402001_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/fed4b823-2e91-3785-2e98-43dac87a59e0.png)

もっとも、表示されるのは子ルート(`contacts/:contactId/destroy`)の`Route`オブジェクトに`errorElement`として直書きした要素(`<div>`)です。実際には、ユーザーに役立つ機能を備えたコンポーネントに差し替えなければなりません。けれど、今回その実装については省きます。

理解いただきたいのは、子ルートのエラーは遡って、もっとも近い親の`errorElement`が描画されるということです。そして、子ルート自体にも`errorElement`は定められ、コンテキストに応じたエラー画面として表示できます。


# インデックスルート
アプリケーションを立ち上げて開いたルートパス(`/`)は、サイドバーの右側が何もない空白です。ルートが`children`をもつとき、はじめは親のパスで表示されます。すると、`<Outlet>`には何も描画されません。パスに合致する子ルートがないからです。

インデックスルートを用いれば、親のデフォルト子ルートとして描くことができます。まず、インデックスルートのモジュール(`src/routes/index.tsx`)をつくりましょう。

```react:src/routes/index.tsx
import type { FC } from 'react';

export const Index: FC = () => {
	return (
		<p id="zero-state">
			This is a demo for React Router.
			<br />
			Check out{' '}
			<a href="https://reactrouter.com">the docs at reactrouter.com</a>.
		</p>
	);
};
```

つぎに、`src/App.tsx`モジュールの`router`です。親ルート(`/`)の`Route`オブジェクトに`children`として、インデックスルートのコンポーネント(`Index`)を加えましょう。このとき、子`Route`オブジェクトの[`index`](https://reactrouter.com/en/main/route/route#index)に`true`を与えるのが、親ルートパス(`/`)のデフォルト子ルートとする定めです。他の子ルートは親とはパスが異なりますので、`<Outlet>`には描画されません(逆に、他の子ルートのパスに遷移すると、親のパスのインデックスルートは表示されなくなるということです)。

```react:src/App.tsx
import { Index } from './routes/index';

const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,

		children: [
			{ index: true, element: <Index /> },

		],
	},
]);
```

アプリケーションのルートのパス(`/`)を開くと、サイドバーの右側に子のインデックスルートが表紙されるようになりました(図003)。

#### 図003■ルートのパス(`/`)でサイドバーの右側に子のインデックスルートが表示される
![qiita_2402001_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/ca5d5964-813f-6775-53d4-3e09dac52a09.png)


# 取り消しボタン
編集画面の[Cancel]ボタンがまだ動いていませんでした。機能はブラウザの[戻る]ボタンと同じです。何もせずに、もとの画面に戻します。ボタンの`onClick`ハンドラに用いるのは、React Routerの[`useNavigate`](https://reactrouter.com/en/main/hooks/use-navigate)フックが返す関数(`navigate`)です。引数に`-1`を与えれば、履歴をひとつ戻ります。

```react:src/routes/edit.tsx
// import { Form, redirect, useLoaderData } from 'react-router-dom';
import { Form, redirect, useLoaderData, useNavigate } from 'react-router-dom';

export const EditContact: FC = () => {

	const navigate = useNavigate();
	return (
		<Form method="post" id="contact-form">

			<p>

				<button
					type="button"
					onClick={() => {
						navigate(-1);
					}}
				>
					Cancel
				</button>
			</p>
		</Form>
	);
};
```

さて、ボタンの`onClick`ハンドラから、フォームが送られるのを防ぐ[`preventDefault()`](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)メソッドは呼び出しませんでした。これは、`<button>`要素の[`type`](https://developer.mozilla.org/docs/Web/HTML/Element/button#type)属性を`button`としたからです。`<button type="button">`には既定の動作がないので、フォームは送信されません。

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


# `URLSearchParams`とGET送信
これまで扱ってきたインタラクティブなユーザーインタフェースは、リンクとフォームのふたつでした。

* リンク: URLを変更する。
* フォーム: データを`action`にPOST送信する。

検索フィールドの機能は、いわばふたつの組み合わせです。

* フォームでURLを変更するだけ。
* データは変えない。

検索フォームのモジュール`src/components/search-form.tsx`は、今のところ標準HTMLの`<form>`要素を用いています。React Routerの`<Form>`コンポーネントではありません。そして、[`<input type="search">`](https://developer.mozilla.org/docs/Web/HTML/Element/input/search)に与えられている属性は`name="q"`です。

このとき、ブラウザがデフォルトでどう動くか確かめましょう。サイドバーの記録リストから、名前をひとつ検索フィールドに入力して[enter]キーを押してみます。URLに加わるのは、`?q=`に続く[`URLSearchParams`](https://developer.mozilla.org/docs/Web/API/URLSearchParams)のクエリです。

```
https://stackblitz-starters-ycxk9t.stackblitz.io/?q=Bill
```

```react:src/components/search-form.tsx
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>

		</div>
	);
};
```

ブラウザはフォームを`<input>`要素の`name`属性にもとづいてシリアライズします。前掲コードで`name`属性の値は`q`でした。そのため、URLには`?q=`というクエリが添えられたのです。

この[`<form>`](https://developer.mozilla.org/docs/Web/HTML/Element/form)要素には、前に扱ったコードと違って`method`属性(`method="post"`)が定められていません。すると、デフォルトの`method`属性値は`get`です。つまり、つぎのドキュメントに向けてブラウザがフォームデータを加えるのは、POSTリクエストの本体ではありません。GETリクエストの[`URLSearchParams`](https://developer.mozilla.org/docs/Web/API/URLSearchParams)です。



# クライアントサイドルーティングのGET送信
クライアントサイドルーティングでフォームを送信し、リストは`loader`によりフィルタリングしましょう。モジュール`src/components/search-form.tsx`の`<form>`要素は`<Form>`コンポーネントに差し替えてください。

```react:src/components/search-form.tsx
export const SearchForm: FC = () => {
	return (
		<div>
			{/* <form id="search-form" role="search"> */}
			<Form id="search-form" role="search">

				{/* </form> */}
			</Form>

		</div>
	);
};
```

`loader`(`src/routes/root.tsx`)は`searchParams`プロパティでリストをフィルタリングします。クエリ引数の値を取り出すのが、[`searchParams.get`](https://developer.mozilla.org/docs/Web/API/URL/searchParams)です。

```react:src/routes/root.tsx
// export const loader: LoaderFunction = async () => {
export const loader: LoaderFunction = async ({ request }) => {
	const url = new URL(request.url);
	const q = url.searchParams.get('q');
	// const contacts = await getContacts(undefined);
	const contacts = await getContacts(q);

};
```

ここで用いたメソッドは、POSTでなくGETでした。したがって、React Routerは`action`は呼び出しません。フォームのGET送信はリンクをクリックしたのと同じで、ただURLが変わるだけです。そのため、フィルタリングのコードは、ルート(`src/routes/root.tsx`)の`action`でなく、`loader`に書きました。

GET送信は、通常のページナビゲーションと変わりません。[戻る]ボタンで前のURLに遷移することもできるのです。


# URLとフォームの状態を同期する
今のコードでは、URLとフォームの状態の同期に問題がふたつあることに気づくでしょう。

1. 検索後に[戻る]ボタンを押したとき:
    * フォームの検索フィールドには入力した値が残っている。
    * サイドバーの記録リストはフィルタリングされていない。
2. 検索したあとにページを読み込み直したとき:
    * フォームの検索フィールドは値が消える。
    * サイドバーの記録リストはフィルタリングされたまま。

先に、上記2の問題です。モジュール`src/routes/root.tsx`の`loader`から`q`の値をオブジェクトに加えて返します。

```react:src/routes/root.tsx
export const loader: LoaderFunction = async ({ request }) => {

	// return { contacts };
	return { contacts, q };
};
```

コンポーネント`SearchForm`(`src/components/search-form.tsx`)は、`useLoaderData`で`q`の値を得て、`<input>`要素の`defaultValue`に与えればよいのです。これで、検索語にページをリロードしても、検索フィールドのクエリ値は消えません。

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

export const SearchForm: FC = () => {
	const { q } = useLoaderData();
	return (
		<div>
			<Form id="search-form" role="search">
				<input

					defaultValue={q}
				/>

			</Form>

		</div>
	);
};
```

つぎに、前述1の問題については、[戻る]ボタンをクリックしたら、検索フィールド(`<input type="search">`)の値は更新すべきです。`SearchForm`(`src/components/search-form.tsx`)に加えた`useEffect`からら、DOMのフォームの状態をReactで直接書き替えます。検索フィールドの値は、`URLSearchParams`と同期するようになりました。

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

export const SearchForm: FC = () => {

	useEffect(() => {
		(document.getElementById('q') as HTMLInputElement).value = q;
	}, [q]);

};
```

ここで、`SearchForm`はコントロールされたコンポーネントではなく、Reactの状態をもたないことが気になるかもしれません(「[コンポーネントがコントロールされているかいないか](https://qiita.com/FumioNonaka/items/611bd5e0cd254c0e9ea0#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%8C%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%81%8B%E3%81%84%E3%81%AA%E3%81%84%E3%81%8B)」参照)。コントロールされたコンポーネントとして扱うことはもちろんできます。けれど、同じふるまいをさせるためのコードは、より煩雑になるでしょう。URLをコントロールしたいわけではありません。ユーザーが押した[戻る]/[進む]ボタンに対応させるだけです。

[コントロールされたコンポーネントを使う場合](https://reactrouter.com/en/main/start/tutorial#synchronizing-urls-to-form-state:~:text=If%20you%27re%20still%20concerned%2C%20expand%20this%20to%20see%20what%20it%20would%20look%20like)、検索フィールドの同期処理は3箇所加えなければなりません。前掲コードなら1箇所で済むのです。


# `onChange`ハンドラでフォーム送信する
ここで、検索フィールドの仕様が変わりました。[enter]キーでフォーム送信するのでなく、キー入力のたびにフィルタリングをかけたいということです。フックの`useNavigate`は、すでに使いました。今回用いるのは、[`useSubmit`](https://reactrouter.com/en/main/hooks/use-submit)です。戻り値の関数(`submit`)を実行して、フォームが送信できます。この関数を`SearchForm`コンポーネント(`src/components/search-form.tsx`)の`onChange`ハンドラから呼び出せばよいでしょう。

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

export const SearchForm: FC = () => {

	const submit = useSubmit();

	return (
		<div>
			<Form id="search-form" role="search">
				<input

					onChange={({ currentTarget: { form } }) => {
						submit(form);
					}}
				/>

			</Form>

		</div>
	);
};
```

これで、検索フィールドに入力するたびに、フォームは送信されます(サンプル002)。`submit`関数の引数はフォーム(`form`)です。`onChange`ハンドラに加えられた[`currentTarget`](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)の要素(`<input>`)から、親ノードの`form`を取り出しました。関数は渡されたフォームをシリアライズして送信します。

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


# 検索にスピナーを加える
実際のアプリケーションの検索では、大きなデータベースから記録を探すことになるかもしれません。すると、データを一度に送れなかったり、フィルタリングに時間がかかったりもするでしょう。このチュートリアル作例では、あえてネットワークの遅れを擬似的に加えています。

ローディングの進行状況が示されないと、検索は遅く感じがちです。データベースを高速化できたとしても、ユーザー側のネットワーク遅延は生じます。開発側からはコントロールできません。ユーザー体験改善のため、インタフェースに素速い検索のフィードバックを加えましょう。ふたたび用いるのは、[`useNavigation`](https://reactrouter.com/en/main/hooks/use-navigation)です。

`SearchForm`コンポーネント(`src/components/search-form.tsx`)に検索スピナーの要素(`<div id="search-spinner">`)はすでに加えてあり、ただし非表示(`hidden={true}`)にしてありました。

```react:src/components/search-form.tsx
import {

	useNavigation,

} from 'react-router-dom';

export const SearchForm: FC = () => {

	const { location } = useNavigation();

	const searching = location && new URLSearchParams(location.search).has('q');

	return (
		<div>
			<Form id="search-form" role="search">
				<input
					id="q"
					className={searching ? 'loading' : ''}

				/>
				{/* <div id="search-spinner" aria-hidden hidden={true} /> */}
				<div id="search-spinner" aria-hidden hidden={!searching} />

			</Form>

		</div>
	);
};
```

`useNavigation`の戻り値から取り出したのは、アプリケーションが新しいURLに遷移して、データを読み込むときにつくられる[`location`](https://reactrouter.com/en/main/utils/location)です。保留されているナビゲーションがなくなれば消えます。[`location.search`](https://github.com/remix-run/history/blob/main/docs/api-reference.md#locationsearch)で調べられるのは、クエリ文字列(`?`に続く`key=value`の組み)です(「[URL: search プロパティ](https://developer.mozilla.org/docs/Web/API/URL/search)」参照)。

検索データを読み込む間、入力フィールドの左端に検索スピナーが表れるようになりました(図004)。

#### 図004■検索中入力フィールド左端に検索スピナーが表れる
![qiita_2402001_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/aa2a279d-5660-cda7-8aa2-9a1386399637.png)


# 履歴スタックを管理する
フォームは検索フィールドに入力するたびに送信されるようになりました。けれど、履歴スタックに積み上がります。"Bill"とタイプし、ひと文字ずつ消し去ると、新たに7つの履歴がスタックに残ってしまうのです(図005)。ユーザーにとって好ましいこととはいえません。

#### 図005■入力ひと文字ごとに履歴スタックが加わる
![qiita_2402001_005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/3080cc82-a9d6-546d-7a00-5f0deb9613c0.png)

これを避けるには、履歴スタックにつぎのページを追加するのでなく、現行の入力と置き替えることです。その場合、`submit`の第2引数オブジェクトに[`replace`](https://reactrouter.com/en/main/components/form#replace)オプションを与えてください。

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

	return (
		<div>
			<Form id="search-form" role="search">
				<input
					id="q"

					onChange={({ currentTarget: { form } }) => {
						const isFirstSearch = q == null;
						// submit(form);
						submit(form, {
							replace: !isFirstSearch,
						});
					}}
				/>

		</div>
	);
};
```

置き替えたいのは検索結果です。検索をはじめた(クエリのない)ページは履歴に残さなければなりません。そのため、クエリの有無(`q == null`)を調べたうえで、置き替えるかどうか(`isFirstSearch`)決めました。

検索フィールドでタイプしても、もはや履歴スタックの入力は増えません。[戻る]ボタンひとつで、フィルタリングされていない検索をはじめる画面に戻れるでしょう。


# 遷移なしにデータを変更する
これまで、データを変えようとするときは、フォームで遷移して、履歴スタックには新たな入力がつくられて加わりました。この処理の流れは一般的です。けれど、遷移することなくデータを変更したい場合も少なくありません。

このようなときに用いるのが[`useFetcher`](https://reactrouter.com/en/main/hooks/use-fetcher)フックです。`loader`や`action`と、ナビゲーションはなしにやりとりできます。各記録につけるお気に入り(★)ボタンは適した例でしょう。記録の作成・削除もページ遷移も要りません。表示されているページのデータを変更したいだけです。

`Favorite`コンポーネント(`src/routes/contact.tsx`)の`<Form>`を[`<fetcher.Form>`](https://reactrouter.com/en/main/hooks/use-fetcher#fetcherform)に置き替えてください。コンポーネントのボタン(`<button>`)には、`name="favorite"`が与えられています。このキーで送られる[`formData`](https://developer.mozilla.org/docs/Web/API/FormData)の値(`value`)は`'false' | 'true'`、`<fetcher.Form>`からの送信は`method="post"`です。

```react:src/routes/contact.tsx
// import { Form, useLoaderData } from 'react-router-dom';
import { Form, useFetcher, useLoaderData } from 'react-router-dom';

const Favorite: FC<{ contact: ContactType }> = ({ contact }) => {
	const fetcher = useFetcher();
	let favorite = contact.favorite;

	return (
		// <Form method="post">
		<fetcher.Form method="post">
			<button
				name="favorite"
				value={favorite ? 'false' : 'true'}
				aria-label={favorite ? 'Remove from favorites' : 'Add to favorites'}
			>
				{favorite ? '★' : '☆'}
			</button>
			{/* </Form> */}
		</fetcher.Form>
	);
};
```

`<fetcher.Form>`に`action`プロパティの定めはないので、フォームが描画されるルート(`src/routes/contact.tsx`)の`action`関数にPOST送信されます。`action`は`request`から取り出した`formData`と、データモジュールの関数`updateContact`によりデータを更新するだけです。

```react:src/routes/contact.tsx
// import { getContact } from '../contacts';
import { getContact, updateContact } from '../contacts';

export const action = async ({ request, params: { contactId } }) => {
	const formData = await request.formData();
	return updateContact(contactId, {
		favorite: formData.get('favorite') === 'true',
	});
};
```

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

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

const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,

		children: [

			{
				path: 'contacts/:contactId',
				element: <Contact />,
				loader: contactLoader,
				action: contactAction,
			},

		],
	},
]);
```

各記録で名前の右にある☆をクリックすると、お気に入り表示となり、サイドバーのリストにも印が加わります(図006)。

#### 図006■記録の名前右側のボタンでお気に入り表示が切り替わる
![qiita_2402001_009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/80999009-388d-2fdd-c79b-f87c5f1e419b.png)

`<fetcher.Form method="post">`と`<Form method="post">`の基本的な機能は同じです。POST送信で`action`を呼び出し、すべてのデータは自動的に再検証されます。エラーの拾い方も変わりありません。ひとつ異なる重要な点は、`<fetcher.Form>`がナビゲーションしないことです。URLは変わらず、履歴スタックにも影響を及ぼしません。


# 楽観的な(Optimistic)UI更新
お気に入りボタンの反応が遅いと感じたかもしれません。ここでも作例には、ネットワークの遅延を擬似的に仕込んでありました。遅れが生じたのは、`Favorite`コンポーネント(`src/routes/contact.tsx`)が更新データ(`contact`)のロードを待ったからです。読み込み中のフィードバックを与える手もあるでしょう。その場合に使えるのは、`navigation.state`と同様の機能を果たす[`fetcher.state`](https://reactrouter.com/en/main/hooks/use-fetcher#fetcherstate)です。

けれど、今回は「楽観的な(Optimistic)UI更新」の手法を用います。`fetcher`が`action`に送るのは更新するフォームデータ(`formData`)です。したがって、[`fetcher.formData`](https://reactrouter.com/en/main/hooks/use-fetcher#fetcherformdata)から新しい値は得られます。状態の更新はネットワーク通信の終わりを待つまでもありません。更新できなかったとき、ユーザーインタフェースがデータをもとに戻せばよいのです。

```react:src/routes/contact.tsx
const Favorite: FC<{ contact: ContactType }> = ({ contact }) => {

	let favorite = contact.favorite;
	if (fetcher.formData) {
		favorite = fetcher.formData.get('favorite') === 'true';
	}

};
```

今度は、お気に入りボタンをクリックすると、すぐに新たな状態に切り替わります。実際のデータがレンダーされるまで、わざわざ待たないからです。上記コードは、`fetcher.formData`が送信されたら、その値をただちに用います。そして、`action`が完了すると、`fetcher.formData`は消滅するのです。実際に書き替えられたデータは、そこで改めて使います。楽観的なUIのコードに不具合があって更新できなかったとしても、もとの正しい状態に戻り、そのままの値は残りません。


# データが見つからないとき(Not Found)
さて、記録が存在しない動的セグメント、たとえば`/contact/not-exist`でルートを遷移するるとどうなるでしょう。開くのは`router`(`src/App.tsx`)のルート(`/`)に加えた[`errorElement`](https://reactrouter.com/en/main/route/error-element)のエラー画面です(図007)。

#### 図007■記録が存在しない動的セグメントでルートを遷移する
![qiita_2402001_006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/439f1102-b666-cafb-0c06-485498eb5f05.png)

ただ、`null`のプロパティ`avatar`が読み込めないと告げられてもよくわかりません。実際には、`src/routes/contact.tsx`の`loader`に渡された`params`の`contactId`が記録データの中に見つからなかったということです。そのため、`Contact`コンポーネントから呼び出された`useLoaderData`は有効な`contact`を返せず(`null`)、`avatar`プロパティも取り出せませんでした。

```react:src/routes/contact.tsx
export const loader: LoaderFunction<Params> = async ({
	params: { contactId },
}) => {
	const contact = await getContact(contactId);
	return { contact };
};

export const Contact: FC = () => {
	const { contact } = useLoaderData() as { contact: ContactType };
	return (
		<div id="contact">
			<div>
				<img key={contact.avatar} src={contact.avatar || undefined} />
			</div>

		</div>
	);
};
```

要は、ページが見つからなかった(Not Found)ということです。コードから明示的にエラーを投げれば、内容がわかりやすく示せます。

```react:src/routes/contact.tsx
export const loader: LoaderFunction<Params> = async ({
	params: { contactId },
}) => {

	if (!contact) {
		throw new Response('', {
			status: 404,
			statusText: 'Not Found',
		});
	}

};
```

#### 図008■404 Not Foundエラー画面が表示された
![qiita_2402001_007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/d5769740-ce9d-ccb2-f6d9-867e54e673e8.png)


# パスなしのルート
前項のエラーページにはサイドバーがありません。サイドバーの右に表示したいときは、ルート(`/`)の`Route`オブジェクトに`children`配列として加えて、`Root`コンポーネントの`<Outlet />`に表示します。このとき、つぎのように入れ子になった`Route`オブジェクトには、`errorElement`と`children`だけで`path`がありません。これが今回のルート構成を実現するパスなしのルートという手法です。


```react:src/App.tsx
const router = createBrowserRouter([
	{
		path: '/',
		element: <Root />,

		children: [
			{
				errorElement: <ErrorPage />,
				children: [
					{ index: true, element: <Index /> },

				],
			},
		],
	},
]);
```

サイドバーの右にNot Foundエラーが表示されるので、エラーは気にせず別の記録を選ぶこともできるようになりました(図009)。まとめたStackBlitzのコードは、以下のサンプル003のとおりです。

#### 図009■サイドバーの右にNot Foundエラーが表示される
![qiita_2402001_008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/4d555d73-f3e9-5096-6914-d4b261367f04.png)

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


# JSXでルートを定める
ルート(`router`)は、オブジェクト以外に、JSXの構文でも定められます。そのJSXを引数として受け取るのが[`createRoutesFromElements`](https://reactrouter.com/en/main/utils/create-routes-from-elements)です。戻り値は、`createBrowserRouter`の引数に渡します。ですから、機能はどちらの構文でも変わりません。お好みでお選びください。

JSXではルートは`<Route>`コンポーネントで、プロパティはその属性として与えます。ただし、`children`プロパティはないので、`<Route>`コンポーネントを入れ子にしてください。

前掲サンプル003のルートをJSXに書き替える場合、修正するモジュールは`src/App.tsx`です。JSXに書き改めたコードは、サンプル004に掲げました。

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

/* const router = createBrowserRouter([
	{

	},
]); */
const router = createBrowserRouter(
	createRoutesFromElements(
		<Route
			path="/"
			element={<Root />}
			loader={rootLoader}
			action={rootAction}
			errorElement={<ErrorPage />}
		>
			<Route errorElement={<ErrorPage />}>
				<Route index element={<Index />} />
				<Route
					path="contacts/:contactId"
					element={<Contact />}
					loader={contactLoader}
					action={contactAction}
				/>
				<Route
					path="contacts/:contactId/edit"
					element={<EditContact />}
					loader={contactLoader}
					action={editAction}
				/>
				<Route path="contacts/:contactId/destroy" action={destroyAction} />
			</Route>
		</Route>
	)
);
```

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