複数のページで構成されたReactアプリケーションを構築する際にはReact単独ではルーティングの機能をもっていないためルーティングライブラリが必要となります。数あるルーティングライブラリの中でReact Routerは最も人気の高いライブラリです。複数のページを持つということはブラウザからアクセスするためのURLが複数存在することになります。アプリケーションを構成するURLにアクセスした場合にどのページコンポーネントのコンテンツを表示させるのかといったルーティングの設定をReact Routerを利用して行います。ログインページやユーザ登録ページなど複数のページが必要なアプリケーションをReactを利用して構築したい場合にはReact Routerを利用することになります。

通常のWEBサーバではページを移動する度にサーバから送られてくるコンテンツをブラウザ上に描写させるためページ全体のリロードが必要になります。しかしReact + React Routerを利用した場合はページを移動するとページ全体のコンテンツがサーバから送られてくるわけではなく最初にアクセスした時にダウンロードしたJavaScriptを使ってページ内で更新が必要な場所のみ更新し、データが必要な場合はfetch関数やaxiosライブラリを利用してサーバからデータの取得を行い描写します。そのためページ全体のリロードを行う必要がなくSPA(シングルページアプリケーション)としてスムーズにページ移動を行うことができます。

本文書ではReact Router v6を初めて設定する人を対象にシンプルなコードを使ってReact Routerの基本について説明を行っています。またReact Router v6.4から新たなルーティングの設定方法であるData APIsが登場しました。Data APIsの設定方法については後半で説明を行なっているのでData APIsのみ知りたい人はそこから読み進めてください。

React RouterのみがReactで利用できる唯一のルーティングライブラリではありません。例えばReactベースのフルスタックフレームワークのNext.jsではReact Routerを利用していません。その他にTanStack Routerというものもあります。
fukidashi

プロジェクトの作成

create-react-app

React Router v6の動作確認を行うためReactプロジェクトの作成をcreate-react-appコマンドを利用して行います。プロジェクトには任意の名前をつけて作成を行ってください。


 % npx create-react-app react-router-6-practise

Vite

Viteを利用してプロジェクトを作成したい場合には以下のコマンドを実行します。


 % npm create vite@latest react-router-6-practise -- --template react

コマンドが完了するとreac-router-6-practiseディレクトリが作成されるのでディレクトリに移動してnpm installコマンドを実行してください。

React Routerのインストール

Reactのプロジェクトのインストールが完了したらプロジェクトフォルダに移動してreact-router-dom@6のインストールを行います。@6を省略してもバージョン6がインストールされます。


 % cd react-router-6-practise
 % npm install react-router-dom@6

package.jsonファイルで動作確認を行ったライブラリのバージョンを確認しておきます。react-router-domのバージョンは6.6.2であることが確認できます。


{
  "name": "react-router-6-practise",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^13.0.0",
    "@testing-library/user-event": "^13.2.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.6.2",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.0"
  },
//略

Reactの動作確認

react-router-domのインストールが完了したら、srcフォルダの中にあるApp.jsx(App.js)ファイルを下記のように更新します。


function App() {
  return (
    <div>
      <h1>Hello React Router v6</h1>
    </div>
  );
}

export default App;

create-react-appを利用した場合はnpm startコマンド、Viteを利用した場合はnpm run devコマンドを実行して開発サーバを起動します。


 % npx start
Compiled successfully!

You can now view react-router-6-practise in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.2.118:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

npm startコマンドを実行するとブウラザが自動起動するので”Hello React Router 6”が表示されるか確認します。React Router v6を確認するための環境の構築は完了です。

Reactの初期ページの表示
Reactの初期ページの表示

Viteの場合は開発サーバのURLはhttp://localhost:5173/です。main.jsxファイルにimportしているindex.cssにより”Hello React Router v6″が中央に表示されるのでmain.jsxファイルからindex.cssファイルのimport文をコメントにするか削除してください。

ルーティングの設定

ユーザがアクセスしたURLによってどのような内容を表示させるかといった処理を行うことをルーティングといいます。ルーティングを設定することで/home, /about/, /contantにアクセスすると異なるページを表示させることができます。

ページコンポーネントの作成

React Routerを設定後、あるURLに対してユーザからアクセスが行われるとページに表示させる内容となるデータを戻す必要があります。その内容はページコンポーネントの中に記述していきます。ページコンポーネントを保存するためsrcフォルダの下にroutesフォルダを作成します。本文書ではroutesとしていますが任意のフォルダ名をつけることができます。ページコンポーネントを保存するのでpagesとしても問題ありません。

routesフォルダにhome.jsx, about.jsx, contact.jsxファイルを作成してください。

それぞれのファイルに下記を内容を記述していきます。


function Home() {
  return <h2>Home</h2>;
}

export default Home;

function About() {
  return <h2>About</h2>;
}

export default About;

function Contact() {
  return <h2>Contact</h2>;
}

export default Contact;

作成したページコンポーネントの内容がブラウザ上に表示されるか確認を行っておきます。


import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';

function App() {
  return (
    <div>
      <h1>Hello React Router v6</h1>
      <Home />
      <About />
      <Contact />
    </div>
  );
}

export default App;

http://localhost:3000にアクセスするとブラウザ上には3つのページコンポーネントに記述した内容が表示されています。

作成したページコンポーネントの表示確認
作成したページコンポーネントの表示確認

はじめてのルーティング設定

Reactのアプリケーションの中でReact Routerによるルーティングの設定が行えるようにsrc¥index.jsファイルにBrowserRouterを設定します。Viteを利用している場合にはmain.jsxファイルで設定します。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

BrowerRouterを設定後App.jsxファイルでRoutes, Routeをimportしてブラウザから(ルート)に対してアクセスがあった場合にHomeコンポーネントの内容を表示するように設定を行います。


import { Routes, Route } from 'react-router-dom';
import Home from './routes/home';
// import About from './routes/about';
// import Contact from './routes/contact';

function App() {
  return (
    <div className="App">
      <h1>Hello React Router v6</h1>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </div>
  );
}

export default App;

ブラウザを利用してlocalhost:3000にアクセスするとHomeの内容が表示されます。/(ルート)に対してReact Routerを設定してルーティングが利用できるようになりました。

/(ルート)へのアクセス
/(ルート)へのアクセス

/(ルート)へのアクセス時だけ表示が行われているのか確認するために適当なURL(ここではtest)を入力してアクセスを行ってください。Hello React Router v6は表示されますがHomeコンポーネントの内容が表示されることはありません。設定したルーティングへのアクセス時のみ内容が表示されるのでルーティング設定が問題なく行われていることがわかります。

ルート以外へのアクセスで表示される内容
ルート以外へのアクセスで表示される内容

/about, /contactのルーティングの追加を行います。これで3つのルーティングがアプリケーションに追加されたことになります。


import { Routes, Route } from 'react-router-dom';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';

function App() {
  return (
    <div className="App">
      <h1>Hello React Router v6</h1>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </div>
  );
}

export default App;

設定後は/aboutにアクセスすると文字列のAbout, /contactにアクセスすると文字列Contactが表示されることを確認してください。表示される内容は各ページコンポーネントに記述した内容です。

React Routerのルーティングによって異なるURLにアクセスするとそのURLに対応したコンテンツを表示できるようになりました。

path=”/”のルーティングについてはindexに置き換えることができます。


<Route index element={<Home />} />

Not Found Routesの設定

先ほどルーティングの動作確認ための適当なURLの/testにアクセスすると”Hello React Router v6″以外は何も表示されませんでした。ページが存在しないURLにアクセスした場合にはアクセスしてきたユーザに何かメッセージを表示したいものです。

新たにroutesフォルダにnomatch.jsファイルを作成します。


function NoMatch() {
  return <h2>このページは存在しません。</h2>;
}

export default NoMatch;

作成したNoMatchコンポーネントをimportしてRouteコンポーネントのelementに設定します。Routeコンポーネントのpathには*(アスタリスク)を設定します。


import { Routes, Route } from 'react-router-dom';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';
import NoMatch from './routes/nomatch';

function App() {
  return (
    <div className="App">
      <h1>Hello React Router v6</h1>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<NoMatch />} />
      </Routes>
    </div>
  );
}

export default App;

ルーティングに存在しないURLにアクセスするとnomatch.jsファイルに記述した内容が表示されます。

ルーティングに存在しないURLへのアクセス
ルーティングに存在しないURLへのアクセス

ルーティングを設定した/(ルート), /about, /contact以外のURLにアクセスするとすべてNoMatchコンポーネントの内容が表示されることになります。存在しないページへのアクセスがあるとメッセージでユーザに伝えることができるようになりました。

ページコンポーネントにprops

Reactでは親コンポーネントから子コンポーネントに対してデータを渡したい場合にpropsを利用して行うことができます。

React Routerのページコンポーネントにもpropsを介してデータを渡すこともできます。


<Route path="/contact" element={<Contact message="Hello Contact" />} />

contact.jsxファイルではpropsを介してmessageを受け取って表示させています。


function Contact(props) {
  return <h2>{props.message}</h2>;
}

export default Contact;

渡されたpropsのmessageを受け取り表示することができます。

propsの表示
propsの表示

propsでデータが渡せることが確認できたらのcontact.jsxファイルは元の状態に戻しておきます。


function Contact() {
  return <h2>Contact</h2>;
}

export default Contact;

リンクの設定

ここまでの設定では設定したルーティングのページにアクセスするためには手動でブラウザのURLを書き換えを行う必要がありました。リンクを利用してページの移動ができるようにナビゲーションリストを追加します。ナビゲーションリストではaタグを利用しhref属性にはルーティングが設定したpathの値を設定します。


import { Routes, Route } from 'react-router-dom';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';
import NoMatch from './routes/nomatch';

function App() {
  return (
    <div className="App">
      <h1>Hello React Router v6</h1>
      <ul>
        <li>
          <a href="/">Home</a>
        </li>
        <li>
          <a href="/about">About</a>
        </li>
        <li>
          <a href="/contact">Contact</a>
        </li>
      </ul>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<NoMatch />} />
      </Routes>
    </div>
  );
}

export default App;

ブラウザで確認すると設定通り各ページへのリンクがページの上部にリスト表示されます。

リンクのリストを追加
リンクのリストを追加

リンクボタンをクリックするとクリックしたページが表示されますが表示される際にページ全体がリロードが行われます。表示される内容が少ないこともありリロードがあっという間に終わってしまうためリロードが行われているかどうか少しわかりにくいかもしれません。ブラウザのタブに表示されているファビコンのiconを見ると再読み込みが行われていることでも判断できます。

aタグではページを移動するページのリロードが行われますがReact RouterのLinkコンポーネントを利用することでページの移動によるリロードはなくなります。

Linkコンポーネントの設定

LinkコンポーネントをimportとしてaタグをLinkタグに置き換えます。さらにaタグを置き換えるだけではなくhref属性からpropsのtoに変更します。


import { Routes, Route, Link } from 'react-router-dom';
//略
<ul>
  <li>
    <Link to="/">Home</Link>
  </li>
  <li>
    <Link to="/about">About</Link>
  </li>
  <li>
    <Link to="/contact">Contact</Link>
  </li>
</ul>

aタグからLinkコンポーネントに変更するとページの移動がスムーズになります。aタグではページの移動の度にページ全体のリロードが行われていましたがLinkコンポーネントではルーティングで設定したページコンポーネントの箇所のみ更新が行われるためです。ブラウザのタブに表示されるファビコンiconの再読み込みがなくなります。

NavLinkコンポーネントの設定

普段利用するWEBアプリケーションでは現在どのページにアクセスしているかがわかるようにメニューの背景色、リンクのテキスト文字の色が変わったりとスタイル設定が行われています。

LinkコンポーネントからNavLinkコンポーネントに変更することで現在アクセスしているページかどうかを確認することができます。NavLinkコンポーネントには設定したリンクが現在アクセスされているかどうかの情報を受け取ることができるのでその情報をstyle、classNameを利用してスタイルの設定を行います。

まずはstyleを利用した設定方法を確認します。styleの中で関数を設定するとisActiveという変数を受け取ることができます。Homeをクリックした場合にはコンソールには”true”、Home以外のリンクをクリックした場合にはコンソールにfalseが表示されます。


import { Routes, Route, Link, NavLink } from 'react-router-dom';
//略
<NavLink style={({ isActive }) => console.log(isActive)} to="/">
  Home
</NavLink>

isActiveの値によって適用するstyleを変更することができます。3項演算子を利用してtrueの場合は文字を青に変更し、falseの場合はなにも適用しないようにundefinedを設定します。falseの時にもstyleを設定することはできます。


<NavLink
  style={({ isActive }) => (isActive ? { color: 'blue' } : undefined)}
  to="/"
>
  Home
</NavLink>

上記のNavLinkを設定するとHomeにアクセスした場合はHomeのリンクの文字が青になることが確認できます。他のページに移動した場合には色は設定されません。

アクセスしているページの色だけ変わる
アクセスしているページの色だけ変わる

classNameの設定方法についても確認しておきます。styleと形は同じですがactiveという名前のclassを設定しているのでclassの設定が必要となります。activeの名前は任意なので好きなclass名を設定してください。


<NavLink
  className={({ isActive }) => (isActive ? 'active' : undefined)}
  to="/about"
>
  About
</NavLink>

cssファイルにactiveクラスを追加するためにApp.cssを利用します。Viteの場合は App.cssファイルの内容を削除してactive classのみ設定してください。


.active {
  color: blue;
}

App.cssファイルをindex.jsxファイルでimortします。


import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './App.css';
//略

/aboutのNavLinkコンポーネントにclassNameの設定を行った場合は/aboutにアクセスした時のみ文字が青になります。

classNameによるスタイルの設定
classNameによるスタイルの設定

ここまでの設定ではHome, Aboutのみスタイルの設定が行われる状態ですが、style, classNameを利用したアクセスページのスタイルの設定方法を理解することができました。

カスタムリンク

NavLinkコンポーネントを利用してアクセスしているページのスタイルの設定を行うことができましたがuseResolvedPath、useMatch Hookを利用することでNavLinkを利用しなくてもアクセスしているページにスタイル設定を行うことできます。

useResolvedPathを利用してアクセスしているページのパスを取得し、useMatchを利用することで現在アクセスしているページとuseResolvedPathを利用して取得したパスがマッチするかチェックしてマッチする場合はオブジェクト、マッチしない場合はnullを戻します。matchに値が入っている時のみ文字の色を青に設定しています。


import {
  Routes,
  Route,
  Link,
  NavLink,
  useResolvedPath,
  useMatch,
} from 'react-router-dom';
//略
function CustomLink({ children, to }) {
  let resolved = useResolvedPath(to);
  let match = useMatch({
    path: resolved.pathname,
    end: true,
  });
  return (
    <div>
      <Link style={{ color: match ? 'blue' : '' }} to={to}>
        {children}
      </Link>
    </div>
  );
}
//略

作成したCustomLinkコンポーネントはNavListコンポーネントの代わりになるのでCustomeLinkタグを利用して設定を行うことができます。


<CustomLink to="/contact">Contact</CustomLink>

CustomLinkコンポーネントを利用したページコンポーネントに対してpropsを渡したい場合はCustomLinkコンポーネントの中でpropsの設定を忘れずに行う必要があります。


function CustomLink({ children, to, ...props }) {
  let resolved = useResolvedPath(to);
  let match = useMatch({ path: resolved.pathname, end: true });

  return (
    <div>
      <Link style={{ color: match ? 'blue' : '' }} to={to} {...props}>
        {children}
      </Link>
    </div>
  );
}

useNavigate

Link, Navlinkコンポーネントを利用することでページの移動を行うことができましたがuserNavigate Hookでもページの移動を行うことができます。

useNavigate Hookを利用することでonClickイベントなどの関数の中でページ移動を行うことができます。

about.jsxファイルでuseNavigateをimportしてボタンをクリックすると/contactに移動できるようnavigate関数の引数に/contactを設定します。


import { useNavigate } from 'react-router-dom';

function About() {
  const navigate = useNavigate();

  return (
    <>
      <h2>About</h2>
      <button onClick={() => navigate('/contact')}>Contact</button>
    </>
  );
}

export default About;

Contactボタンが表示されるのでボタンをクリックすると/contactに移動します。

/contactに移動できるボタンを追加
/contactに移動できるボタンを追加

ネスト化

React Router v6で追加された機能でReact Routerを使いこなす上で重要なルーティングのネスト化の設定方法を確認していきます。

ルーティング/postsを追加し/postsにアクセスした際にposts(記事)の一覧が表示されpost(記事)はそれぞれ固有のidを持っているので、一覧表示されている各post(記事)のtitleをクリックすると/posts/1, /posts/2のようにダイナミックなURLの変更があってもidに応じた内容のページが表示されるように設定を行っていきます。

ルーティングの追加

新たにルーティング/postsを追加しroutesフォルダにposts.jsxファイルを作成します。


function Posts() {
  return <h2>Posts</h2>;
}

export default Posts;

App.jsファイルでposts.jsxファイルをimportしてナビゲーションリストへの追加とルーティングの追加を行います。


import { Routes, Route, NavLink } from 'react-router-dom';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';
import Posts from './routes/posts';
import NoMatch from './routes/nomatch';

function App() {
  return (
    <div>
      <h1>Hello React Router v6</h1>
      <ul>
        <li>
          <NavLink
            style={({ isActive }) => (isActive ? { color: 'blue' } : undefined)}
            to="/"
          >
            Home
          </NavLink>
        </li>
//略
        <li>
          <NavLink
            className={({ isActive }) => (isActive ? 'active' : undefined)}
            to="/posts"
          >
            Posts
          </NavLink>
        </li>
      </ul>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="/posts" element={<Posts />} />
        <Route path="*" element={<NoMatch />} />
      </Routes>
    </div>
  );
}
export default App;

/postsにアクセスすると文字列Postsが表示されます。

ルーティングpostsを追加後の画面
ルーティングpostsを追加後の画面

Outletの設定

/postsをネスト化(この後設定を行う)して/posts/postにアクセスした場合に表示される内容を記述するためにroutesフォルダにpost.jsxファイルを作成します。


function Post() {
  return <h2>Single Post</h2>;
}

export default Post;

ネスト化する場合postsのルーティングを設定したRouteコンポーネントの中にpostのルーティングを追加します。Postコンポーネントのimportも忘れずに行います。


//略
import Post from './routes/post';
//略

<Route path="/posts" element={<Posts />}>
  <Route path="/post" element={<Post />} />
</Route>
//略

ブラウザ上に何も表示されず、エラーがデベロッパーツールのコンソールに表示されます。


Uncaught Error: Absolute route path "/post" nested under path "/posts" is not valid. An absolute child route path must start with the combined path of all its parent routes

パス”/posts”の下にネスト化された”/post”に対して絶対ルートパスの設定はダメだといっているのでpostのルーティングで設定しているpathの”/post”から”/”を削除してもう一度確認します。


<Route path="/posts" element={<Posts />}>
  <Route path="post" element={<Post />} />
</Route>

設定を変更するとエラーは解消されるので/posts/postにアクセスを行います。Postコンポーネントの内容は表示されますがPostコンポーネントの内容は表示されません。

/posts/postにアクセスした時の画面
/posts/postにアクセスした時の画面

Postコンポーネントの内容を表示させるにはOutletコンポーネントを親コンポーネントであるPostsコンポーネントで設定する必要があります。


import { Outlet } from 'react-router-dom';

function Posts() {
  return (
    <>
      <h2>Posts</h2>
      <Outlet />
    </>
  );
}

export default Posts;

Outletコンポーネントを設定することでPostsコンポーネントの内容が表示されるようになります。

Outletコンポーネントによりネスト化されたコンポーネントの内容が表示
Outletコンポーネントによりネスト化されたコンポーネントの内容が表示

Routerコンポーネントの中にRouterコンポーネントを入れることでネスト化できることがわかりました。

ダイナミックルーティング

/posts/postにアクセスするとPostコンポーネントに表示された内容が表示されます。/posts/1, /posts/2のようにidによって表示させる内容を動的に変更したい場合の設定方法を確認します。

現在の設定では/posts/1にアクセスすると”このページは存在しません。”と表示されます。idの値が変わってもPostコンポーネントの内容を表示させるためには下記のようにルーティングを設定する必要があります。pathの設定値に:(コロン)に任意の名前をつけることで/posts/1, /posts/2,…にアクセスしてもPostコンポーネントの内容が表示されるようになります。/posts/postでもアクセスすることは可能です。


<Route path="/posts" element={<Posts />}>
  <Route path=":postId" element={<Post />} />
</Route>
URLを動的に変更してもPostコンポーネントの内容が表示される
URLを動的に変更してもPostコンポーネントの内容が表示される

/posts/以下の値を変更してもページを表示することができるようになりましたが/posts以下の値によって表示内容を変えるためには/posts/以下に入っている値を取り出す必要があります。次はその方法を確認していきます。

useParams

URLに含まれているpostIDはuseParams Hookを利用して取得することができます。useParamsを利用するためにはimportする必要があります。


import { useParams } from 'react-router-dom';

function Post() {
  const params = useParams();
  console.log(params);
  return <h2>Single Post</h2>;
}

export default Post;

/posts/10にアクセスしデベロッパーツールのコンソールを確認するとルーティングのpathで指定したpostIdにURLで指定した10が入っていることが確認できます。

useParams HookでpostIdを取得
useParams HookでpostIdを取得

分割代入を利用してブラウザ上にpostIdを表示することができます。


import { useParams } from 'react-router-dom';

function Post() {
  const { postId } = useParams();
  return <h2>Single Post {postId}</h2>;
}

export default Post;

postIdを利用して外部からデータを取得してPostコンポーネントに表示させてみましょう。

外部リソースにはJSONPlaceHolderを利用します。無料のサービスでhttps://jsonplaceholder.typicode.com/posts/1にアクセスするとJSONでデータが戻されます。どのようなJSONが戻されるかは上記のURLを直接ブラウザのURLに設定することで確認することができます。


import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

function Post() {
  const { postId } = useParams();
  const [post, setPost] = useState('');

  useEffect(() => {
    const fetchPost = async () => {
      const res = await fetch(
        `https://jsonplaceholder.typicode.com/posts/${postId}`
      );
      const data = await res.json();
      setPost(data);
    };
    fetchPost();
  }, [postId]);
  
  return (
    <>
      <h2>Single Post</h2>
      <div>
        <p>ID:{post.id}</p>
        <p>タイトル:{post.title}</p>
        <p>内容:{post.body}</p>
      </div>
    </>
  );
}

export default Post;

URLの数字を変更すると異なる内容が表示されるので1〜100まで間の数字をpostIdに設定してください。下記では50を設定しています。

JSONPlaceHolderからデータ取得し表示
JSONPlaceHolderからデータ取得し表示

postIdの値が動的に変わってもページに表示される内容を変更できるようになりました。

Index Routesの設定

postIdの値を利用してPostコンポーネントの表示内容を動的に変更できるようになりました。URLに入るpostIdの数字を手動ではなくリンクからアクセスできるようにPostの一覧を/postsにアクセスした時に表示できるように設定を行っていきます。

現在の設定で/postsにアクセスすると以下のように表示されます。

ルーティングpostsを追加後の画面
ルーティングpostsの画面

上記の画面にposts一覧を表示させるためにposts.jsxファイルからJSONPlaceHolderにアクセスして一覧情報が表示できるように設定を行います。取得したデータはmap関数を利用して展開し展開したidとtitleにLinkコンポーネントを設定しクリックすると各ページに移動できるように設定を行っています。


import { useEffect, useState } from 'react';
import { Link, Outlet } from 'react-router-dom';

function Posts() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchPosts = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await res.json();
      setPosts(data);
    };
    fetchPosts();
  }, []);

  return (
    <>
      <h2>Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link to={`/posts/${post.id}`}>
              {post.id}:{post.title}
            </Link>
          </li>
        ))}
      </ul>
      <Outlet />
    </>
  );
}

export default Posts;

/postsにアクセスするとJSONPlaceHolderから取得したposts一覧がリスト表示されます。

postsの一覧表示
postsの一覧表示

一覧の中からidが3のタイトルのリンクをクリックするとURL(/posts/3)が変わりますが表示される画面に変化がありませんが画面をスクロールをするとpostIdを使って取得したpostが表示されます。

posts一覧とpostの情報が表示
posts一覧とpostの情報が表示

/posts/3にアクセスした場合はposts一覧の表示が必要ないので/posts/にアクセスした場合のみposts一覧を表示させる必要があります。

新たにpostindex.jsxファイルを作成しposts一覧を取得する処理をposts.jsxファイルから削除してこのファイルに移動します。


import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';

function PostIndex() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchPosts = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/posts');
      const data = await res.json();
      setPosts(data);
    };
    fetchPosts();
  }, []);

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link to={`/posts/${post.id}`}>
            {post.id}:{post.title}
          </Link>
        </li>
      ))}
    </ul>
  );
}

export default PostIndex;

posts一覧を取得する処理をpostindex.jsxファイルに移動させた後のposts.jsxファイルは下記のようになります。


import { Outlet } from 'react-router-dom';

function Posts() {
  return (
    <>
      <h2>Posts</h2>
      <Outlet />
    </>
  );
}

export default Posts;

App.jsファイルしpostindex.jsxファイルをimportしてルーティングを追加します。その際にpathの設定を行わずindexを設定します。


import Posts from './routes/posts';
import Post from './routes/post';
import PostIndex from './routes/postindex';
//略
<Route path="/posts" element={<Posts />}>
  <Route index element={<PostIndex />} />
  <Route path=":postId" element={<Post />} />
</Route>

設定は完了です。/postsにアクセスするとpostの一覧が表示されます。idが50のタイトルリンクをクリックします。

postsの一覧表示
postsの一覧表示

今回はpostsの一覧が表示されずpostのみ表示されていることが確認できます。

JSONPlaceHolderからデータ取得し表示
一覧は表示されない

ネスト化を行った際にルート(今回は/posts)にアクセス時にだけ表示させる内容を設定したい場合はルーティングにindexを設定する必要があることがわかりました。

Relative Pathの設定

postindex.jsxファイルではpostに対するリンクに絶対パスを設定していました。


<ul>
  {posts.map((post) => (
    <li key={post.id}>
      <Link to={`/post/${post.id}`}>
        {post.id}:{post.title}
      </Link>
    </li>
  ))}
</ul>

postに対するリンクから/post/を削除しても相対パスとして設定されるので問題なくページに移動することができます。


<ul>
  {posts.map((post) => (
    <li key={post.id}>
      <Link to={`${post.id}`}>
        {post.id}:{post.title}
      </Link>
    </li>
  ))}
</ul>

レイアウトの設定

最初に作成したHome, About, Contactコンポーネントを利用してレイアウトの設定方法を確認していきます。

ネスト化で利用してPosts, Post, Postindexコンポーネントとそれらに対する設定はない状態で動作確認していきます。
fukidashi

レイアウトもコンポーネントなので別ファイルにすることもできますが、App.jsファイルの中にLayoutコンポーネントを作成します。Layoutコンポーネントではpropsのchilderenを利用して画面の中央に表示されるようにflexを利用しています。


const Layout = ({ children }) => {
  return (
    <div style={{ display: 'flex', justifyContent: 'center' }}>{children}</div>
  );
};

作成したLayoutコンポーネントをRoutesコンポーネントの中のRouteをラップします。


<Routes>
  <Layout>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
    <Route path="*" element={<NoMatch />} />
  </Layout>
</Routes>

残念ながらこの設定では以下のメッセージが表示されます。RoutesコンポーネントのはRouteコンポーネントからRoute.Fragmentである必要があります。


Uncaught Error: [Layout] is not a component. All component children of<Routes> must be a <Route> or <Route.Fragment>

上記の方法ではうまくいかなかったので次はelement属性にLayoutコンポーネントを利用して各ページコンポーネントをラップします。


<Route path="/" element={<Layout><Home /></Layout>} />

確認するとLayoutで設定したflexboxが適用されHomeが画面中央に表示されます。

Layoutで設定したスタイルが適用
Layoutで設定したスタイルが適用

さらに別の方法として先ほど確認したネストの設定を利用して行います。elementにLayoutコンポーネントを設定したRouteを追加してレイアウトを適用したいRouteをラップします。


<Routes>
  <Route element={<Layout />}>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
    <Route path="*" element={<NoMatch />} />
  </Route>
</Routes>

エラーはでませんが各ページコンポーネントの内容は表示されません。ネスト化する際はchildrenではなく、Outletが必要なのでchildrenからOutletに変更します。


import {
  Routes,
  Route,
  Link,
  NavLink,
  useResolvedPath,
  useMatch,
  Outlet,
} from 'react-router-dom';
//略
const Layout = () => {
  return (
    <div style={{ display: 'flex', justifyContent: 'center' }}>
      <Outlet />
    </div>
  );
};

再度Home, About, Contactさらに存在しないURLにアクセスしてもLayoutのスタイルが適用されたページとして表示されます。

エラーメッセージにはこのLayoutは適用したくないという場合は下記のように設定することで対応することができます。


<Routes>
  <Route element={<Layout />}>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route> path="/contact" element={<Contact />} />
  </Route>
  <Route path="*" element={<NoMatch />} />
</Routes>

elementにLayoutを設定したRouteコンポーネントにpath=”/”を設定することもできます。elementにHomeを設定したRouteコンポーネントにはindexを設定します。


<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
  </Route>
  <Route path="*" element={<NoMatch />} />
</Routes>

新たに別のLayout2コンポーネントを定義してある一部のルーティングに設定するということも可能です。Layout2ではjustify-contentの値を’end’に設定しているので/contactにアクセスするとContactコンポーネントの内容が右側に表示されます。


//略
const Layout2 = () => {
  return (
    <div style={{ display: 'flex', justifyContent: 'end' }}>
      <Outlet />
    </div>
  );
};
//略
<Routes>
  <Route element={<Layout />}>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Route>
  <Route element={<Layout2 />}>
    <Route path="/contact" element={<Contact />} />
  </Route>
  <Route path="*" element={<NoMatch />} />
</Routes>

下記のようにLayoutコンポーネントを設定したRouteコンポーネントにpathを設定することも可能です。


<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="/about" element={<About />} />
  </Route>
  <Route element={<Layout2 />}>
    <Route path="/contact" element={<Contact />} />
  </Route>
  <Route path="*" element={<NoMatch />} />
</Routes>

その他のフック

useLocation

useLocation Hookを利用するとlocationオブジェクトが入っているということなのでオブジェクトにどのような情報が含まれているのか確認してみましょう。

contact.jsファイルの中でuseLocationをimportして中身を確認します。


import { useLocation } from 'react-router-dom';

function Contact() {
  const location = useLocation();
  console.log(location);

  return <h2>Contact</h2>;
}

export default Contact;

オブジェクトの中にはhash, key, pathname, search, stateが含まれています。pathnameは値を見ればアクセスしたURLであることがわかりますがそのほかの値についてはここまでの説明では出てきていません。

useLocationから取得できるオブジェクトを確認
useLocationから取得できるオブジェクトを確認

searchの値についてはqueryパラメータの値が保存されているのでabout.jsで設定したnavigate関数で設定を行なってみます。


<button> onClick={() => navigate('/contact?api_key=eimaieU9')}>
  Contact
</button>

ボタンをクリックして/contactに移動するとsearchに値が入っていることが確認できます。

searchの値の確認
searchの値の確認

stateの値はnavigate関数の第二引数で渡すことができるので設定を行っています。


<button
  onClick={() => navigate('/contact?api_key=eimaieU9', { state: 'test' })}
>
  Contact
</button>
</>

stateに値が入っていることが確認できます。

stateの値を確認
stateの値を確認

navigate関数で設定を行いましたがLinkやNavLinkでも設定を行うことができます。NavLinkの場合は下記のようにtoにオブジェクトとして設定を行うことでlocationオブジェクトのserachとstateに設定した値が入ります。


<NavLink>
  className={({ isActive }) => (isActive ? 'active' : undefined)}
  to={{
    pathname: '/contact',
    search: '?api_key=eimaieU9',
    state: 'test',
  }}
>

useSearchParams

URLに含まれるパラメータを取得する際に利用できるだけではなくuseSearchParams Hookを利用することでURLにパラメータを追加することができます。

contact.jsファイルでuseSearchParams Hookを利用するため下記の設定を行います。searchParamsに保存された値はgetメソッドで取得することができます。


import { useSearchParams } from 'react-router-dom';

function Contact() {
  const [searchParams, setSearchParams] = useSearchParams();

  console.log(searchParams.get('product_name'));

  return <h2>Contact</h2>;
}

export default Contact;

これだけでは何を行っているのかわからないと思いますが/contactにアクセスします。searchParamsのproduct_nameには何も入っていないのでnullが表示されます。

パラメータの値を確認
パラメータの値を確認

URLにパラメータをつけてproduct_nameに値が保存されるのか確認します。URLに直接入力します。contact?product_name=iPadとしています。

パラメータに設定した値が表示
パラメータに設定した値が表示

コンソールには”iPad”が表示されます。ここまででURLに設定した値が取得できることがわかりました。次はReact側からURLのパラメータを設定する方法を確認します。

input要素を追加してchangeイベントを利用して文字を入力する度にhandleChange関数を実行します。handleChange関数ではeventオブジェクトからinput要素にアクセスして入力した値を取得しsetSearchParamsでproduct_nameに入力した値を設定しています。


import { useSearchParams } from 'react-router-dom';
function Contact() {
  const [searchParams, setSearchParams] = useSearchParams();
  const paramsValue = searchParams.get('product_name') || '';
  console.log(searchParams.get('product_name'));

  const handleChange = (event) => {
    const product_name = event.target.value;
    if (product_name) {
      setSearchParams({ product_name: event.target.value });
    } else {
      setSearchParams({});
    }
  };

  return (
    <div>
      <h2>Contact</h2>
      <input type="text" onChange={handleChange} value={paramsValue} />
    </div>
  );
}

export default Contact;

アクセスするとパラメータの値が設定されている場合はparamsValueに設定したinput要素のvalueの値によりiPadがコンソールに表示されます。

input要素に初期値が設定
input要素に初期値が設定

input要素の文字を1文字削除してください。それに合わせてURLに入っているパラメータの値も一文字削除されていることがわかります。

input要素で文字を削除。URLに反映。
input要素で文字を削除。URLに反映。

input要素の文字をiPhoneに変更するとURLの文字もiPhoneに変更されます。

input要素の文字をiPhoneに更新
input要素の文字をiPhoneに更新

useSearchParams Hookを利用することでReact側からURLのパラメータを設定できることがわかりました。

useSearchParamsを利用して検索機能を実装してみましょう。製品情報を持った配列productsを定義します。


const products = [
  {
    id: 1,
    product_name: 'iPhone',
    price: 1000,
  },
  {
    id: 2,
    product_name: 'iPad',
    price: 500,
  },
  {
    id: 3,
    product_name: 'iPod',
    price: 40,
  },
  {
    id: 4,
    product_name: 'MacBook',
    price: 2000,
  },
];

定義したproductsをmap関数を利用して展開してブラウザ上に表示させます。


<div>
  <h2>Contact</h2>
  <input type="text" onChange={handleChange} value={paramsValue} />
  <ul>
    {products.map((product) => (
      <li key={product.id}>
        {product.product_name}/{product.price}
      </li>
    ))}
  </ul>
</div>

productsに入ったすべての製品情報が一覧表示されます。

製品一覧を表示
製品一覧を表示

URLに含まれるパラメータの値を利用して検索を行うためsearchParamsを利用した関数searchProductsを追加します。filter関数で展開したproductのproduct_nameにsearchProducts.get(‘product_name’)から取得した値が含まれているかチェックをしています。searchProducts.get(‘product_name’)に値がない場合はすべての商品が戻されることになります。


const searchProducts = () => {
  return products.filter((product) => {
    return product.product_name.includes(
      searchParams.get('product_name') || ''
    );
  });
};

map関数を利用してproductsをsearchProducts関数に変更します。


<ul>
  {searchProducts().map((product) => (
    <li key={product.id}>
      {product.product_name}/{product.price}
    </li>
  ))}
</ul>

input要素に入力する文字がproduct_nameに含まれているもののみ表示されることを確認してください。iPと入力したい場合は”iP”を含む3つの製品が表示されます。

iPを含む製品のみ表示
iPを含む製品のみ表示

検索機能を実装することができました。そのままURLにhttp://localhost:3000/contact?product_name=iPをペーストしても検索が実行され”iP”を含む製品のみ表示されます。

Data APIsでの設定

React Router v6.4からルーティングの設定方法の異なるData APIsが登場しました。Data APIsを利用することを推奨しているので今後React Routerを利用する場合はData APIsを理解する必要があります。Data APIsの動作確認を行うため本文書でも先ほどまで利用していたBrowserRouterではなくcreateBrowserRouterを利用してルーティングの設定を行います。Routeコンポーネントも利用できますがオブジェクトを利用してルーティングの設定を行うことができます。

プロジェクトの作成

Viteを利用してプロジェクトの作成を行います。npm create viteコマンドでプロジェクトの作成を行います。Reactを利用するため–templateにreactを選択しています。プロジェクト名も任意の名前をつけることができますがここでは”react-router-practise”という名前にしています。


 % npm create vite@latest react-router-6-practise -- --template react
Need to install the following packages:
create-vite@5.1.0
Ok to proceed? (y) y

Scaffolding project in /Users/mac/Desktop/react-router-6-practise...

Done. Now run:

  cd react-router-6-practise
  npm install
  npm run dev

コマンドを実行するとreact-router-6-practiseディレクトリが作成されるので移動してnpm installコマンドを実行します。


 % npm install

react-router-domのインストール

React Routerを利用するためにはreact-router-domライブラリのインストールが必要となります。npmコマンドを利用してreact-router-domライブラリのインストールを行います。


 % npm install react-router-dom

インストールが完了したらpackage.jsonファイルで今回利用したパッケージのバージョンを確認しておきます。react-router-domのバージョンは6.21.0であることがわかります。


{
  "name": "react-router-6-practise",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.21.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.55.0",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "vite": "^5.0.8"
  }
}

動作確認

srcディレクトリのApp.jsxファイルを以下のように更新します。


function App() {
  return <h1>Hello React Router v6</h1>;
}

export default App;

main.jsxファイルでimportしているindex.cssのスタイル設定のため開発サーバを起動すると文字列が画面中央に表示されるのでmain.jsxファイルのimport文をコメントアウトしておきます。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
// import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

開発サーバを起動するためにnpm run devコマンドを実行します。


% npm run dev

> react-router-6-practise@0.0.0 dev
> vite


  VITE v5.0.9  ready in 680 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

ブラウザからhttp://localhost:5173にアクセスすると”Hello React Router v6”の文字列が表示されます。

Reactの動作確認
Reactの動作確認

Routerページコンポーネントの作成

/, /about, /contactのURLにアクセスした際に表示されるページコンポーネントを作成します。本文書ではroutesディレクトリの下にhome.jsx, about.jsx, contact.jsxファイルを作成します。

それぞれのファイルに下記を内容を記述していきます。


function Home() {
  return <h2>Home</h2>;
}

export default Home;

function About() {
  return <h2>About</h2>;
}

export default About;

function Contact() {
  return <h2>Contact</h2>;
}

export default Contact;

Routeコンポーネントの設定

ルーティングの設定に先程作成したHome, About, Contactコンポーネントを利用します。3つのコンポーネントに加えてレイアウトの役割を持つRootコンポーネントを利用します。routesフォルダにroot.jsxファイルを作成してナビゲーションを設定し、Outletコンポーネントを設定しています。ページのリンクを設定する場合はaタグではなくLinkコンポーネントを使用します。aタグでも設定は可能ですがaタグのリンクを使ってページの移動した場合はページ全体の再読み込みが行われます。Linkコンポーネントを利用した場合にはページ移動にはページ全体の再読み込みは行われずスムーズにページ移動することができます。


import { Link, Outlet } from 'react-router-dom';
const Root = () => {
  return (
    <>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/contact">Contact</Link>
        </li>
      </ul>
      <Outlet />
    </>
  );
};

export default Root;

ルーティングの設定はmain.jsxファイルで行います。App.jsxファイルでも同様の方法で設定することができます。

ルーティングの設定

createBrowserRouterの引数には配列の中にオブジェクトを記述することでBrowser Routerを作成してルーティングを設定します。作成したrouterはRooterProviderのrouter propsに設定します。”/”にアクセスした場合にRootコンポーネントが表示されることになります。


import React from 'react';
import ReactDOM from 'react-dom/client';
// import './index.css'
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './routes/root';

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

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

ブラウザで確認するとRootコンポーネントに記述したナビゲーションのみ表示されます。

ルートコンポーネントのみ表示
ルートコンポーネントのみ表示

Home, About, Contactはchildrenプロパティに配列で設定を行なっていきます。Homeコンポーネントは”/”にアクセスした時に表示を行うためpathではなくindexプロパティの値をtrueに設定することでindex Routeの設定ができます。


import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './routes/root';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'contact',
        element: <Contact />,
      },
    ],
  },
]);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

リンクをクリックすることでページ移動を行うことができます。Aboutのリンクをクリックするとスムージに/から/aboutページに移動します。

リンクを使ったページ移動
リンクを使ったページ移動

React Routerの動作確認
childrenに設定した各コンポーネントはRootコンポーネントの<Outlet />を設定した部分に表示されます。もしRootコンポーネントのOutletタグを削除した場合にはページの移動はできますがRootコンポーネントに記述したリンクのみ表示されます。

root.jsxファイルからOutlietタグを削除した場合
root.jsxファイルからOutlietタグを削除した場合

createRoutesFromElementsによる設定

createRoutesFromElementsはRouteコンポーネントを利用してルーティングを設定することができます。オブジェクトではなくコンポーネントで設定したい場合にこちらを利用することができます。


import React from 'react';
import ReactDOM from 'react-dom/client';
// import './index.css'
import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from 'react-router-dom';
import Root from './routes/root';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route index element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Route>
  )
);
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

ブラウザ上に表示されたナビゲーションリンクをクリックすることで各ページに移動することができます。

React Routerの動作確認
React Routerの動作確認

エラーページ

ルーティングに存在しないページにアクセスした場合には以下のメッセージがブラウザ上に表示されます。メッセージを確認するとRouteコンポーネントのerrorElement propsでこのメッセージ画面とは異なる画面が作成できると記述されています。

ルーティングの設定にないURLにアクセスした場合のエラー
ルーティングの設定にないURLにアクセスした場合のエラー

routesフォルダにerror-page.jsファイルを作成して以下のコードを記述します。


import { useRouteError } from "react-router-dom";

export default function ErrorPage() {
  const error = useRouteError();
  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コンポーネントをmain.jsxファイルでimportしてルーティングのerrorElementに設定します。


import React from 'react';
import ReactDOM from 'react-dom/client';
// import './index.css'
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './routes/root';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';
import ErrorPage from './routes/error-page';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'contact',
        element: <Contact />,
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

設定後、再度ルーティングに存在しないURLにアクセスするとerror-page.jsファイルで記述した内容が表示されます。

作成したエラーページの表示
作成したエラーページの表示

loaderによるデータの取得

Data APIsではloader関数を利用してデータの取得を行うことができます。これまでのReact Routerではデータ取得に関する機能は持っていなかったのでloader関数の機能追加は非常にインパクトがありました。

loaderによるデータの取得の動作確認を行うためmain.jsxファイルに設定したルーティングに新たにpostsを追加します。postsに対応するコンポーネントPostsをimportしています。


import React from 'react';
import ReactDOM from 'react-dom/client';
// import './index.css'
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './routes/root';
//略
import Posts from './routes/posts';

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

importしたPostsコンポーネントの内容を記述するためにroutesディレクトリにposts.jsxファイルを作成して以下を記述します。


function Posts() {
  return (
    <>
      <h2>Posts</h2>
    </>
  );
}

export default Posts;

ルーティングを追加したのでroot.jsxのナビゲーションにもリンクを追加します。


import { Link, Outlet } from 'react-router-dom';
const Root = () => {
  return (
    <>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/contact">Contact</Link>
        </li>
        <li>
          <Link to="/posts">Posts</Link>
        </li>
      </ul>
      <Outlet />
    </>
  );
};

export default Root;

ブラウザから/postsにアクセスすると”Posts”の文字列が表示されます。

追加した/postsの確認
追加した/postsの確認

Postsコンポーネントの内容がブラウザ上に表示されることが確認できたのでposts.jsxファイルにloader関数を追加してexportします。loader関数は任意の名前をつけることができます。データはJSONPlaceHolderから100件のPostデータを取得しています。


export async function loader() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { posts };
}

function Posts() {
  return (
    <>
      <h2>Posts</h2>
    </>
  );
}

export default Posts;
JSONPlaceHolderは無料で利用できるサービスでJSONPlaceHolderが提供するURLにアクセスするとJSONデータが戻されるので動作確認に利用することができます。
fukidashi

exportしたloader関数はmain.jsxでimportを行い、ルーティングpostsのloaderプロパティに設定します。importしたloader関数の名前はpostsLoaderにしています。


import React from 'react';
//略
import Posts, { loader as postsLoader } from './routes/posts';
const router = createBrowserRouter([
  {
//略
      {
        path: 'posts',
        element: <Posts />,
        loader: postsLoader,
      },
    ],
  },
]);

ブラウザから/postsにアクセスしてブラウザのデベロッパーツールのネットワークタブを確認するとJSONPlaceHolderへのアクセスが行われデータが取得できていることが確認できます。

取得したデータはposts.jsxファイルでuseLoaderData Hookを利用して取得することができます。useLoaderData Hookから取得したpostsデータはmap関数で展開しています。


import { useLoaderData } from 'react-router-dom';
export async function loader() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { posts };
}

function Posts() {
  const { posts } = useLoaderData();
  return (
    <>
      <h2>Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            {post.id}:{post.title}
          </li>
        ))}
      </ul>
    </>
  );
}

export default Posts;

ブラウザにはJSONPlaceHolderから取得したPosts一覧が表示されます。loader関数はPostsコンポーネントが描写される前に実行されます。

loader関数を利用して取得したデータの表示
loader関数を利用して取得したデータの表示

loader関数とuseLoaderData Hookを組み合わせることで外部リソースから取得したデータをブラウザ上に表示することができるようになりました。

Dynamic RoutingによるLoader設定

loader関数を利用することで/postsにアクセスするとPost一覧を表示することができるようになりました。Post一覧に表示されているタイトルをクリックすると個別ページが表示されるように設定を行なっていきます。個別ページのURLは/posts/1, /posts/2,…というように動的に変わることになります。そのためDynamic Routingと呼ばれます。

ルーティングはpostsのchildとしてmain.jsxファイルで設定します。pathは/posts/1, /posts/2のように動的に変わるので先頭に:(コロン)をつけてpostIdとします。対応するコンポーネントはPostコンポーネントでimportを行っています。


//略
import Post from './routes/post';
//略
{
  path: 'posts',
  element: <Posts />,
  loader: postsLoader,
  children: [
    {
      path: ':postId',
      element: <Post />,
    },
  ],
},

routesディレクトリpost.jsxファイルを作成して以下のコードを記述します。


function Post() {
  return (
    <>
      <h2>Single Post</h2>
    </>
  );
}

export default Post;

ブラウザから/posts/2にアクセスを行ってください。/postsにアクセスした時と同じ100件分のPostデータが表示されPostコンポーネントに記述した”Single Post”の文字列は見つかりません。

“Single Post”の文字列を表示するためには/routes/posts.jsxファイルにOutletタグを追加する必要があります。


import { Outlet, useLoaderData } from 'react-router-dom';
export async function loader() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { posts };
}

function Posts() {
  const { posts } = useLoaderData();
  return (
    <>
      <h2>Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            {post.id}:{post.title}
          </li>
        ))}
      </ul>
      <Outlet />
    </>
  );
}

export default Posts;

設定後、ブラウザから/posts/2にアクセスするとPost一覧の下にpost.jsファイルに記述した”Single Post”が表示されます。

/posts/2へのアクセス
/posts/2へのアクセス

Post一覧のタイトルをクリックすると個別ページに移動できるようにroutes/posts.jsxファイルでリンクの設定を行います。


import { Link, Outlet, useLoaderData } from 'react-router-dom';

export async function loader() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { posts };
}

function Posts() {
  const { posts } = useLoaderData();
  return (
    <>
      <h2>Posts</h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link to={`/posts/${post.id}`}>
              {post.id}:{post.title}
            </Link>
          </li>
        ))}
      </ul>
      <Outlet />
    </>
  );
}

export default Posts;

Index Routeの設定

Post一覧と”Single Post”が同じページに表示されているのでPost一覧は/posts/1, /posts/2…にアクセスした場合に表示されないように設定を追加します。routesフォルダに新たにpostindex.jsファイルに作成します。postindex.jsxファイルでloader関数を実行するように設定を行います。


import { Link, useLoaderData } from 'react-router-dom';

export async function loader() {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts`);
  const posts = await res.json();
  return { posts };
}

function PostIndex() {
  const { posts } = useLoaderData();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link to={`/posts/${post.id}`}>
            {post.id}:{post.title}
          </Link>
        </li>
      ))}
    </ul>
  );
}

export default PostIndex;

posts.jsxファイルからloader関数を含め、postindex.jsxファイルで設定した処理を削除します。


import { Outlet } from 'react-router-dom';

function Posts() {
  return (
    <>
      <h2>Posts</h2>
      <Outlet />
    </>
  );
}

export default Posts;

main.jsxファイルでルーティングの更新を行います。postindexを追加してIndex Routeとして設定します。


import React from 'react';
import ReactDOM from 'react-dom/client';
// import './index.css'
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './routes/root';
import Home from './routes/home';
import About from './routes/about';
import Contact from './routes/contact';
import ErrorPage from './routes/error-page';
import Posts from './routes/posts';
import Post from './routes/post';
import PostIndex, { loader as postsLoader } from './routes/postindex';

const router = createBrowserRouter([
//略
      {
        path: 'posts',
        element: <Posts />,
        children: [
          {
            index: true,
            element: <PostIndex />,
            loader: postsLoader,
          },
          {
            path: ':postId',
            element: <Post />,
          },
        ],
      },
    ],
  },
]);
//略

main.jsxファイルを更新後に/postsにアクセスするとPost一覧が表示され、/posts/1, /posts2にアクセスすると”Single Post”の文字列のみ表示されるようになります。

個別ページの表示
個別ページの表示

loader関数の設定

個別ページにデータを表示させるためにLoader関数を設定します。ルーティングに設定した”:postId”はLoader関数の引数のparamsで取得することができるので以下のように記述することができます。


import { useLoaderData } from 'react-router-dom';

export async function loader({ params }) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.postId}`
  );
  const post = await res.json();
  return { post };
}

function Post() {
  const { post } = useLoaderData();
  return (
    <>
      <h2>Single Post</h2>
      <div>
        <p>ID:{post.id}</p>
        <p>タイトル:{post.title}</p>
        <p>内容:{post.body}</p>
      </div>
    </>
  );
}

export default Post;

main.jsxファイルのルーティング設定でPostコンポーネントで追加したloader関数のimportして設定を行います。


import React from 'react';
//略
import Posts from './routes/posts';
import PostIndex, { loader as postsLoader } from './routes/postindex';
import Post, { loader as postLoader } from './routes/post';

const router = createBrowserRouter([
  {
//略
      },
      {
        path: 'posts',
        element: <Posts />,
        children: [
          {
            index: true,
            element: <PostIndex />,
            loader: postsLoader,
          },
          {
            path: ':postId',
            element: <Post />,
            loader: postLoader,
          },
        ],
      },
    ],
  },
]);

//略

設定後、/posts/2にアクセスするとloader関数で取得したデータが表示されます。

Loader関数で取得したデータの表示
Loader関数で取得したデータの表示

Dynamic Routingでのloader関数を利用したデータ取得方法を確認することができました。

Not Foundエラーの表示

Post一覧に存在しないpostId(/posts/1000)を持つページにアクセスすると各項目に値が入っていない画面が表示されます。

空のページの表示
空のページの表示

fetch関数でリクエストに成功したか確認するためにres.okの値を確認し、res.okがfalse(リクエストに失敗)した場合にはエラーをthrowするように設定します。


export async function loader({ params }) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.postId}`
  );
  if (!res.ok) {
    throw Error('Not Found');
  }

  const post = await res.json();

  return { post };
}

設定後、/posts/1000にアクセスするとエラーページの内容が表示されます。

エラーページの表示
エラーページの表示

エラーをthrowしたことでpathの”/”に設定しerrorElementが実行されています。errorElementは各ルーティング毎に設定することができるので”:postId”のルーティングの親にあたる”posts”のルーティングにerrorElementの設定を行います。


{
  path: 'posts',
  element: <Posts />,
  errorElement: <ErrorPage />,
  children: [
    {
      index: true,
      element: <PostIndex />,
      loader: postsLoader,
    },
    {
      path: ':postId',
      element: <Post />,
      loader: postLoader,
    },
  ],
},

ブラウザで確認するとPostsコンポーネントの表示位置エラーページの内容が表示されていることがわかります。

エラー内容の表示場所の確認
エラー内容の表示場所の確認

さらにエラーがthrowされた”:postId”のルーティングのErrorElementの設定を行います。


{
  path: 'posts',
  element: <Posts />,
  errorElement: <ErrorPage />,
  children: [
    {
      index: true,
      element: <PostIndex />,
      loader: postsLoader,
    },
    {
      path: ':postId',
      element: <Post />,
      loader: postLoader,
            errorElement: <ErrorPage />,
    },
  ],
},

ブラウザで確認するとPostコンポーネントが表示される場所にエラーの内容が表示されていることがわかります。

エラーメッセージの表示場所(Post)
エラーメッセージの表示場所(Post)

ErrorElementの設定をどのルーティングに設定するかによってエラーが表示させる場所が変わることが確認できました。

actionによるデータの追加

loaderを利用してデータを取得することができました。Webアプリケーションを構築する場合にはデータの取得だけではなくがデータを追加/更新/削除が必要となります。その場合にはactionを利用することができます。フロントエンドのReact上ではデータの追加/更新/削除を行うことができないので、actionの動作確認を行うためバックエンド用にExpressサーバの環境を構築します。

Expressサーバの構築

新たにExpress用のプロジェクトを作成するために任意の場所にreact-router-6-expressフォルダを作成します。作成後にnpm init -yコマンドを実行してpackage.jsonファイルを作成します。メインファイルであるindex.jsファイルも作成します。


 % mkdir react-router-6-express
 % cd react-router-6-express 
 % npm init -y
 % touch index.js

Expressサーバを構築するために以下のパッケージをインストールします。nodemonを利用することでファイルの更新を監視しファイルの更新が行われると自動で再読み込みが行われます。同じURLでもおこなるPORTからのアクセスは許可されないのでcorsを利用することでReactからExpressサーバへの接続を許可するために利用します。


 % npm install express nodemon cors

パッケージをインストール後、package.jsonファイルのscriptを更新します。


{
  "name": "react-router-6-express",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "nodemon": "^3.0.2"
  }
}

index.jsファイルにExpressを起動するために必要となるコードを記述します。


const express = require('express');
const cors = require('cors');

const app = express();
const port = 3000;

app.use(cors());
app.use(express.json());

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

index.jsファイルを更新後、npm startコマンドを実行するとExpressサーバが起動します。


 %  % npm start

> react-router-6-express@1.0.0 start
> nodemon index.js

[nodemon] 3.0.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node index.js`

ルーティングの追加(Express)

Expressサーバの/todosにアクセスがあった場合にTodo一覧を戻す設定を行います。


const express = require('express');
const cors = require('cors');

const app = express();
const port = 3000;

app.use(cors());
app.use(express.json());

const todos = [
  {
    id: 1,
    title: 'Learn React Router',
    completed: false,
  },
  {
    id: 2,
    title: 'Learn SvelteKit',
    compolted: false,
  },
];

app.get('/todos', (req, res) => res.send(todos));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

ブラウザからhttp://localhost:3000/todosにアクセスしてもTodo一覧の情報が表示されます。

ブラウザから/todosにアクセスした時に戻されるデータ
ブラウザから/todosにアクセスした時に戻されるデータ

ルーティングの追加(React)

ExpressサーバからTodo一覧を取得できるようにroutesフォルダにtodos.jsファイルを作成します。loader関数を利用してExpressサーバにアクセスを行っています。


import { useLoaderData } from 'react-router-dom';

export async function loader() {
  const res = await fetch('http://localhost:3000/todos');
  const todos = await res.json();
  return { todos };
}

const Todos = () => {
  const { todos } = useLoaderData();
  return (
    <>
      <h2>Todo</h2>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </>
  );
};

export default Todos;

作成したtodos.jsをmain.jsxでimportを行う/todosとしてルーティングを追加します。


//略
import Contact from './routes/contact';
import Todos, { loader as todosLoader } from './routes/todos';
//略
const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'contact',
        element: <Contact />,
      },
      {
        path: 'todo',
        element: <Todo />,
        loader: todoLoader,
      },
//略

ルーティング後にhttp://localhost:5173/todosにアクセスするとExpressサーバから取得したTodo一覧が表示されます。

Expressサーバから取得したTodo一覧
Expressサーバから取得したTodo一覧

loaderを利用してExpressサーバからデータを取得して表示することができました。

Formの設定

react-router-domからimportしたFormを利用してtodos.jsxファイルにフォームの作成を行います。


import { Form, useLoaderData } from 'react-router-dom';

export async function loader() {
  const res = await fetch('http://localhost:3000/todos');
  const todos = await res.json();
  return { todos };
}

const Todos = () => {
  const { todos } = useLoaderData();
  return (
    <>
      <h2>Todo</h2>
      <Form method="post">
        <input name="title" placeholder="title" />
        <br />
        <button type="submit">Submit</button>
      </Form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </>
  );
};

export default Todos;

ブラウザから確認するとフォームが表示されていることが確認できます。

フォームコンポーネントによるフォームの表示
フォームコンポーネントによるフォームの表示

さらにブラウザのデベロッパーツールの要素を確認するとmethodとactionが設定されているformタグを確認することができます。

Formコンポーネントの確認
Formコンポーネントの確認

input要素に文字列を入力して”Submit”ボタンをクリックしてください。ページがリロードされ”Method Not Allowed”のエラーメッセージが表示されます。

Method Not Allowedのエラーメッセージ
Method Not Allowedのエラーメッセージ

action関数の設定

フォームに入力した値についてはaction関数からアクセスすることができます。しかしaction関数を設定しただけでは先程と同じエラーとなります。


export async function action({ request }) {
  const formData = await request.formData();
  const title = formData.get('title');
  console.log(title);
  return '';
}

この後はloaderを設定した方法と同様の方法でmain.jsxファイルでルーティングにactionの設定を行います。


//略
import Todos, {
  loader as todosLoader,
  action as todosAction,
} from './routes/todos';
//略

const router = createBrowserRouter([
//略
      {
        path: 'todo',
        element: <Todo />,
        loader: todoLoader,
        action: todoAction,
      },
//略

表示される入力フォームに文字列を入力して”Submit”ボタンをクリックするとコンソールに入力した文字列が表示されます。入力した文字列はrequestのformDataに保存されていることが確認できました。

actionの動作確認
actionの動作確認

formDataに保存されている入力データをExpressサーバに送信できるようにfetch関数の設定を行います。formDataに保存された名前と値はObject.fromEntriesを利用してオブジェクトとして取り出すことができます。


const data = Object.fromEntries(await request.formData());
//dataの中身
{title: 'learn SolidJS'}

fetch関数を利用してPOSTリクエストとしてデータを送信します。


export async function action({ request }) {
  const data = Object.fromEntries(await request.formData());
  const res = await fetch('http://localhost:3000/todos', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });
  const todo = await res.json();

  return { todo };
}

Expressサーバ側でPOSTリクエストを受け付けることができる/todosルーティングを追加します。


const express = require('express');
const cors = require('cors');
const crypto = require('crypto');

const app = express();
const port = 3001;

app.use(cors());
app.use(express.json());

const todos = [
  {
    id: 1,
    title: 'Learn React Router',
    completed: false,
  },
  {
    id: 2,
    title: 'Learn SvelteKit',
    compolted: false,
  },
];

app.get('/todos', (req, res) => res.send(todos));
app.post('/todos', (req, res) => {
  const todo = {
    id: crypto.randomUUID(),
    title: req.body.title,
    completed: false,
  };
  todos.push(todo);
  res.status(200).send(todo);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

設定後にフォームに文字列を入れて”Submit”ボタンをクリックすると入力した文字列がTodo一覧に即座に反映されます。これはactionの処理が完了した後にuserLoaderDataが自動で実行され、データの再取得が行われるためです。自動取得によりバックエンドのデータとフロントエンド上のデータを同期することができます。

データの追加
データの追加

Form, actionによるデータの追加方法を確認することができました。

action関数の戻り値についてはuseActionData Hookを利用して取得することができます。


import { Form, useActionData, useLoaderData } from 'react-router-dom';
//略
const Todo = () => {
  const { todos } = useLoaderData();
  const actionData = useActionData();
  console.log(actionData);
  return (
    <>
      <h2>Todo</h2>
      <Form method="post">
        <input name="title" placeholder="title" />
        <br />
        <button type="submit">Submit</button>
      </Form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </>
  );
};

コンソールには追加したTodoの情報が表示されます。

useActionDataから取得した値を表示
useActionDataから取得した値を表示

すべてのReact Router v6, v6.4からの新機能に関するすべての動作確認を行ったわけではありませんがReact Router v6の基本的な設定方法を理解することができました。