準備

TypeScript版のCreate React App を使用します。
下記でプロジェクトディレクトリを作成。

$ yarn create react-app react-location-test --template typescript
$ cd react-location-test

続いてライブラリのインストール。
「React Location」とAPI通信するために「Axios」も入れておきます。

$ yarn add @tanstack/react-location axios

バージョンはそれぞれこんな感じです。
typescript: 4.4.2
react: 17.0.2
@tanstack/react-location: 3.7.0
axios: 0.26.1

一番シンプルなルーティング

最初に単純にページ(コンポーネント)の表示を切り替えるだけのルーティング設定をしてみます。

表示されるページ、ホームページと記事一覧ページコンポーネントを作ります。

src/pages/Home.tsx

import React from 'react'

const Home: React.VFC = () => {
	return (
		<div>
			<h1>ホーム</h1>
		</div>
	)
}

export default Home

src/pages/Posts.tsx

import React from 'react'

const Posts: React.VFC = () => {
	return (
		<div>
			<h1>記事一覧</h1>
		</div>
	)
}

export default Posts

この二つのページを切り替えられるようにルーターの設定をします。
routes.tsxを作成し次のようにします。

src/routes.tsx

import { Route, ReactLocation } from '@tanstack/react-location'
import Home from './pages/Home'
import Posts from './pages/Posts'

export const location = new ReactLocation()

export const routes: Route[] = [
  { path: '/', element: <Home /> },
  { path: '/posts', element: <Posts /> },
]

routes変数にルーターの設定をしています。
下記の挙動になることを想定しています。

パス コンポーネント
/ Homeコンポーネント
/posts Postsコンポーネント

App.tsxを次のように編集します。

src/App.tsx

import { Router, Outlet, Link } from '@tanstack/react-location'
import { routes, location } from './routes'

const App = () => {
	return (
	<div className="App">
		<Router routes={routes} location={location}>
			<header>
				<ul>
					<li><Link to="/">ホーム</Link></li>
					<li><Link to="posts">記事一覧</Link></li>
				</ul>
			</header>
			<Outlet />
		</Router>
	</div>
	)
}

export default App

Linktoroutesで設定したパスを指定することで、指定したコンポーネントに遷移します。
Outletにはパスにマッチしたコンポーネントが表示されます。

これで画面の切り替えができるようになりました。

下層ページのルート設定

次に記事一覧の下に記事詳細ページを作ってみます。

routes.tsxから編集します。

src/routes.tsx

import { Route, ReactLocation } from '@tanstack/react-location'
import Home from './pages/Home'
import Posts from './pages/Posts'
import Post from './pages/Post'

export const location = new ReactLocation()

export const routes: Route[] = [
	{
		path: '/',
		element: <Home />
	}, {
		path: '/posts',
		element: <Posts />,
		children: [
			{
				path: ':id',
				element: <Post />
			}
		]
	}
]

下層ページを作りたい場合はchildrenの中に記述していきます。
記事詳細は「posts/1」「posts/2」とかIDによって動的に切り替えたいので、path:idにします。

次に記事詳細ページのPostコンポーネントを作ります。
ひとまず受け取ったIDをそのまま表示してみます。

src/pages/Post.tsx

import React from 'react'
import { useMatch } from '@tanstack/react-location'

const Post: React.VFC = () => {

	const { id } = useMatch().params

	return (
		<div>
			<h2>投稿詳細</h2>
			<p>記事{id}</p>
		</div>
	)
}

export default Post

IDはuseMatch().paramsで取得することができます。

詳細ページの準備ができたので、一覧ページから遷移できるようにリンクを追加しましょう。

src/pages/Posts.tsx

import React from 'react'
import { Link, Outlet } from '@tanstack/react-location'

const Posts: React.VFC = () => {
	return (
		<div>
			<h1>記事一覧</h1>
			<ul>
				<li>
					<Link to="/posts/1">記事1</Link>
					<Link to="/posts/2">記事2</Link>
					<Link to="/posts/3">記事3</Link>
				</li>
				<Outlet />
			</ul>
		</div>
	)
}

export default Posts

これで各詳細ページへ遷移できるようになります。

LoaderでAPIのデータを取得

React Locationの特徴の一つにLoaderという機能があります。
これを使用することでページの表示前にAPIからデータの取得することができます。
APIには「JSONPlaceholder」を使用ます。

取得するAPIのTypeから設定します。

src/types/Post.tsx

export type PostType = {
	id: number
	title: string
	body: string
}

APIの取得処理も切り出しておきましょう。

src/api/PostApi.ts

import axios from 'axios'
import { PostType } from '../types/Post'

// 記事一覧取得
const getPosts = async () => {
	return await axios
		.get<PostType[]>('https://jsonplaceholder.typicode.com/posts?_limit=5')
		.then(r => r.data)
}

// 記事詳細取得
const getPost = async (id: number) => {
	return await axios
		.get<PostType>(`https://jsonplaceholder.typicode.com/posts/${id}`)
		.then(r => r.data)
}

export {
	getPosts,
	getPost
}

作成したAPIをルーターのLoaderに設定をします。
LocationGenericsは各ページでロードしたデータを使用するのに必要です。

src/api/PostApi.ts

import { Route, ReactLocation, MakeGenerics } from '@tanstack/react-location'
import Home from './pages/Home'
import Posts from './pages/Posts'
import Post from './pages/Post'
import type { PostType } from './types/Post'
import { getPosts, getPost } from './api/PostApi'

export type LocationGenerics = MakeGenerics<{
	LoaderData: {
		posts: PostType[]
		post: PostType
	}
}>

export const location = new ReactLocation()

export const routes: Route[] = [
	{
		path: '/',
		element: <Home />
	},{
		path: '/posts',
		element: <Posts />,
		loader: async () => ({
			posts: await getPosts()
		}),
		children: [
			{
				path: ':id',
				element: <Post />,
				loader: async ({params: {id}}) => ({
					post: await getPost(Number(id))
				})
			}
		]
	}
]

各ページでロードしたデータを表示してみます。

src/pages/Posts.tsx

import React from 'react'
import { Link, Outlet, useMatch } from '@tanstack/react-location'
import { LocationGenerics } from '../routes'

const Posts: React.VFC = () => {
	const { posts } = useMatch<LocationGenerics>().data
	
	return (
		<div>
			<h1>記事一覧</h1>
			<ul>
				{posts?.map(post => (
					<li key={post.id}>
					<Link to={post.id}>{post.title}</Link>
					</li>
				))}
			</ul>
			<Outlet />
		</div>
	)
}

export default Posts

src/pages/Post.tsx

import React from 'react'
import { useMatch, Link } from '@tanstack/react-location'
import { LocationGenerics } from '../routes'

const Post: React.VFC = () => {
	const { post } = useMatch<LocationGenerics>().data

	return (
		<div>
			<h2>投稿詳細</h2>
			{
			post
				? <div>
					<p>{post.title}</p>
					<div>{post.body}</div>
				  </div>
				: <div>記事はありません</div>
			}
			<Link to="/posts">記事一覧に戻る</Link>
		</div>
	)
}

export default Post

ローディング&エラー表示

初期設定での挙動はAPIの取得が完了した後、画面遷移されます。
APIの取得しているときはローディング中的な表示をしたいですね。
全体で同じ挙動にする場合はRouterを下記のように設定します。

src/App.tsx

const Loading = () => (
	<div>ローディング...</div>
)

const Error = () => (
	<div>エラー</div>
)

const App = () => {
	return (
		<div className="App">
			<Router
				routes={routes}
				location={location}
				defaultPendingMs={0}
				defaultPendingElement={<Loading />}
				defaultErrorElement={<Error />}
			>
				...
			</Router>
		</div>
	)
}

defaultPendingMsを0にすることで、ローディング完了前にページ遷移をします。
defaultPendingElementにローディング中のコンポーネントを指定します。
APIエラー時の表示はdefaultErrorElementに設定したコンポーネントを表示できます。

このオプションはroutesのpendingMspendingElementオプションで各ページ単位で設定することもできます。

各ページの表示部分で細かく調節したい場合はuseMatchisLoadingで制御することもできます。

const {
	data: { posts },
	isLoading
} = useMatch<LocationGenerics>()

if (isLoading) {
	return <div>ローディング</div>
}

ビルドファイルの分割

規模が大きくなるとWebpackなどでビルドしたファイルサイズが肥大化し初回の読み込み時間が長くなるということがあります。
ルートのelementを下記ようにすればページ毎にコードを分割して、アクセスがあったときに読み込むということができます。

src/routes.tsx

element: () => import('./pages/Posts').then((mod) => ),
		

React Locationにはその他にもいろいろな機能があります。
詳しくは下記ドキュメントをご確認ください。

Overview | React Location | TanStack