React Routerは複数のページで構成されたReactアプリケーションを構築する際に利用するライブラリです。React Routerを利用することでReactアプリケーションに複数のページを持たせることができます。複数のページを持つということはブラウザからアクセスするためのURLが複数存在することになります。どのURLにアクセスした場合にどのコンポーネントの内容を表示させるのかといった設定をReact Routerを利用して行います。

通常のWEBサーバではページ移動の度にサーバから送られてくる情報をブラウザ上に描写させるためページ全体のリロードが必要になります。しかしReact + React Routerを利用した場合はJavaScriptを使ってページ内で更新が必要な場所のみ更新を行うことができるためページ全体のリロードを行う必要がなくなりSPA(シングルページアプリケーション)としてスムーズにページ移動を行うことができます。

本文書ではReact Router v6を初めて設定する人を対象にシンプルなコードを使ってReact Routerの基本について説明を行っています。

プロジェクトの作成

React Router v6の動作確認を行うためReactプロジェクトの作成を行います。プロジェクトには任意の名前をつけて作成を行ってください。


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

React Routerのインストール

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


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

Reactの動作確認

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


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

export default App;

npm startコマンドでReactアプリケーションの開発サーバを起動します。


 % 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.

ブウラザからhttp://localhost:3000にアクセスしてHello React Router 6が表示されるか確認します。React Router v6を確認するための環境の構築は完了です。

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

ルーティングの設定

ユーザがアクセスしたURLによってどのような内容を戻すのかといった処理を行うことをルーティングといいまさす。

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

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

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

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


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を設定します。


import ReactDOM from 'react-dom';
import App from './App';

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

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

BrowerRouterを設定後App.jsファイルで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が表示されることを確認してください。ページのコンポーネントに記述した内容です。

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


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

Not Found Routesの設定

先ほどルーティングの動作確認ための適当なURLの/testにアクセスすると”Hello React Router v6″以外は何も表示されませんでした。ページがないことを伝えるためにpathに*(アスタリスク)を設定し、新たにnomatch.jsファイルを作成してNoMatchコンポーネントをimportしてRouteコンポーネントのelementに設定します。


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

export default NoMatch;

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にアクセスすると下記のメッセージが表示されます。

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

/, /about, /contact以外のURLにアクセスするとすべてNoMatchの内容が表示されることになります。

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

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


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

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


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

export default Contact;

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

propsの表示
propsの表示

リンクの設定

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


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>
  );
}

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

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

リンクボタンをクリックするとクリックしたページが表示されますが表示される際にページ全体がリロードが行われます。表示される内容が少ないこともありリロードがあっという間に終わってしまうためリロードが行われているかどうか少しわかりにくいですがLinkコンポーネントをaタグと置き換えることで動作の違いが実感できると思います。

Linkコンポーネントの設定

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


<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コンポーネントではルーティングで設定したページコンポーネントの箇所のみ更新が行われるためです。

NavLinkコンポーネントの設定

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

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

まずはstyleを利用した設定方法を確認します。styleの中で関数を設定するとisActiveという変数を受け取ることができます。そのリンクにアクセスしている場合にはtrueしていない場合にはfalseが入ります。


<NavLink style={({ isActive }) => console.log(isActive)} to="/">

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


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

上記のNavListを設定するとHomeにアクセスした場合はHomeのリンクの文字が青になることが確認できます。

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

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


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

cssファイルにactiveクラスを追加するためにApp.cssを利用します。


.active {
  color: blue;
}

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


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

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

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

style, classNameを利用したアクセスページの装飾方法を理解することができました。

カスタムリンク

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

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


function CustomLink({ children, to }) {
  let resolved = useResolvedPath(to);
  let match = useMatch({
    path: resolved.pathname,
    end: true,
  });
  return (
    <div>
      <Link> style={{ color: match ? 'blue' : 'none' }} to={to}>
        {children}
      </Link>
    </div>
  );
}

作成したCustomLinkはNavListの代わりになるので下記のように設定を行うことができます。


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

propsを渡したい場合は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' : 'none' }} to={to} {...props}>
        {children}
      </Link>
    </div>
  );
}

useNavigate

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

about.jsファイルで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.jsファイルを作成します。


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

export default Posts;

App.jsファイルでposts.jsファイルを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 className="App">
      <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.jsファイルを作成します。


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コンポーネントの内容が表示されるようになります。


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

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からデータ取得し表示

Index Routesの設定

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

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

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

上記の画面にposts一覧を表示させるためにposts.jsファイルから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.jsファイルを作成しposts一覧を取得する処理をposts.jsファイルから削除してこのファイルに移動します。


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 kye={post.id}>
          <Link to={`/posts/${post.id}`}>
            {post.id}:{post.title}
          </Link>
        </li>
      ))}
    </ul>
  );
}

export default PostIndex;

posts.jsファイルは下記のようになります。


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

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

export default Posts;

App.jsファイルしpostindex.jsファイルを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.jsファイルではpostに対するリンクに絶対パスを設定していました。


<ul>
  {posts.map((post) => (
    <li kye={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コンポーネントを利用してレイアウトの設定方法を確認していきます。

コンポーネントなので別ファイルにすることもできますが、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>

エラーはでませんが各ページコンポーネントは表示されません。ネストの時はOutletが必要なのでLayoutコンポーネントに追加します。


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>

新たに別のLayout2コンポーネントを定義してある一部のルーティングに設定するということも可能です。


<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>

その他のフック

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”を含む製品のみ表示されます。

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