「Reactで簡単なプログラムは書けるようになったけど画面遷移のあるサイトを作りたい」
「Reactでホームページを作っている例を見かけるけど、どういう仕組みで動いているのか気になる」
「ReactRouterDomがバージョンの違いでどの書き方が正しいのかググっていてわからなくなった」
本日はそんな方に向けてReactRouterDomを使った画面遷移の作り方を解説していきます。
React Router DomはReactアプリケーションにおいて、ナビゲーションやルーティングを実現するための重要なツールセットです。
これを理解し活用することで、シームレスなページ遷移やコンポーネントの表示を実現することができます。
本記事では、React Router Domの基本的な使い方に焦点を当て、特に useParams
、useNavigate
、そしてLink
といった重要な機能について詳しく解説していきます。
またそもそもSPAとはどういう仕組みなのかもお話していきますので、裏側がわかりにくいモダンJRの理解が一層深まるはずです。
React Router Domを効果的に使いこなすことで、より洗練されたReactアプリケーションを開発するための手助けとなるでしょう。それでは、基本から順に見ていきましょう。
また動画もあるので必要な方はご覧ください。
React-Router-Domを使う方法
まずはライブラリのインストールを行います。
npm i react-router-dom
ReactRouterDomでは画面遷移を管理するための専用のコンポーネントがあります。
そちらをApp.jsなどプロジェクトの起点となるファイルにて以下のように書きます。
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Top from './screens/reactRouterDom/Top';
import Post from './screens/reactRouterDom/Post';
import Posts from './screens/reactRouterDom/Posts';
import Contact from './screens/reactRouterDom/Contact';
function App() {
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path="/" element={<Top />}></Route>
<Route path="/contact" element={<Contact />}></Route>
<Route path="/posts" element={<Posts />}></Route>
<Route path="posts/:id" element={<Post />}></Route>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
BrowserRouter, Routes, Routeという3種類のコンポーネントをReactRouterDomからインポートして使っています。
一番重要なのはRouteコンポーネントで、こちらでは各ページ(コンポーネント)のパスの設定を行なっていて、pathにはパス名を記載しelementにはコンポーネントを書きます。
今回はTop.js、Contact.js、Posts.js、Post.jsの4種類の画面がある想定です。
基本的にはトップを「/」としてパスを書くだけですが投稿画面のような一覧と詳細に分かれているものについては、詳細のパスを「/:id」というように書くことで投稿画面の画面遷移を可能にします。
続いてTop.jsをトップ画面としてメニューを作っていきます。
メニューについてはLinkという特別なコンポーネントをReactRouterDomからインポートすることでaタグを自動生成できます。
import React from 'react';
import { Link } from 'react-router-dom';
const Top = () => {
return (
<div>
<ul>
{/* aタグを生成 */}
<Link to="/">
<li>トップ</li>
</Link>
<Link to="/contact">
<li>お問い合わせ</li>
</Link>
<Link to="/posts">
<li>お知らせ</li>
</Link>
</ul>
</div>
);
};
export default Top;
Linkコンポーネントのtoには遷移するパスを指定します。
aタグのhrefのように書くのですが、ルートを起点に書くことに注意してください。
ちなみに手動でURLを入力した際に間違ったURLを書いた場合に404ページを作っておきます。
今回はNotFound.jsというファイルに404ページを担当してもらいます。
import React from 'react';
const NotFound = () => {
return <div>NotFound</div>;
};
export default NotFound;
404ページへの移動もRouteコンポーネントで作っておく必要があり、path=”/*”とすることで存在しないパス名は自動的に特定のファイルに移動させることができます。
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Top from './screens/reactRouterDom/Top';
import Post from './screens/reactRouterDom/Post';
import Posts from './screens/reactRouterDom/Posts';
import Contact from './screens/reactRouterDom/Contact';
// ここを追加
import NotFound from './screens/reactRouterDom/NotFound';
function App() {
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path="/" element={<Top />}></Route>
<Route path="/contact" element={<Contact />}></Route>
<Route path="/posts" element={<Posts />}></Route>
<Route path="posts/:id" element={<Post />}></Route>
{/* ここを追加 */}
<Route path="/*" element={<NotFound />}></Route>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
手動で存在しないパスを書くとNotFound.jsに辿り着きます。
続いて投稿機能についてPosts.jsで以下のように書くことでidを持たせた詳細ページへの遷移が可能になります。
import React from 'react';
// ここを追加
import { Link } from 'react-router-dom';
const Posts = () => {
let id = 1;
return (
<div>
Posts
<br />
<Link to={`/posts/${id}`}>2024年のお知らせ</Link>
</div>
);
};
export default Posts;
上記コードでは変数idというものに手動で番号を代入していますが、こちらはDBやAPI上で表現されているIDが入ることになるでしょう。
あとはLinkのtoを変数idを使った動的なパスにして書くだけです。
詳細ページであるPost.jsは以下のような感じです。
import React from 'react';
import { useParams } from 'react-router-dom';
const Post = () => {
const params = useParams();
return <div>Post {params.id}</div>;
};
export default Post;
ReactRouterDomではコンポーネントだけでなくフックも用意されていて、一覧ページから渡されたIDを受け取るにはuseParamsというフックを使用します。
useParamsは初期値無しで定数paramsのように代入して使用します。
定数paramsはオブジェクト形式になっているので「params.id」と書くことでIDの値を取り出すことができます。
コンソールでも見ておきましょう。
import React from 'react';
// ここを追加
import { useParams } from 'react-router-dom';
const Post = () => {
const params = useParams();
// ここを追加
console.log(params);
return <div>Post {params.id}</div>;
};
export default Post;
このようにしてLinkコンポーネントがHTMLのaタグの代わりになって画面を移動できるわけです。
実はReactRouterDomにはuseNavigateというフックを使ったブラウザバックも実装できます。
Contact.jsというページで以下のように書いてみます。
import React from 'react';
// ここを追加
import { useNavigate } from 'react-router-dom';
const Contact = () => {
const navigate = useNavigate();
return (
<div>
Contact<button onClick={() => navigate(-1)}>戻る</button>
</div>
);
};
export default Contact;
onClickイベント内でuseNavigateは関数として使用できて引数に「-1」を入れるとブラウザバックになります。
「戻る」ボタンなどに使える仕様ですね。
useNavigate、useLocation、useSearchParamsを使ったテクニック
ReactRouterDomで画面遷移のやり方がわかったところで他にもある便利なフックを紹介していきます。
Reactで現在のページのメニュー名の色を変える方法
まずはメニューの作り方ですがLinkコンポーネントの他にNavLinkというコンポーネントでもHTMLのaタグの役割を担ってくれます。
LinkタグをNavLinkタグに変えてみます。
import React from 'react';
// ここを追加
import { NavLink } from 'react-router-dom';
const Top = () => {
return (
<div>
<ul>
<NavLink to="/">
<li>トップ</li>
</NavLink>
<NavLink to="/contact">
<li>お問い合わせ</li>
</NavLink>
</ul>
</div>
);
};
export default Top;
NavLinkタグでもLinkタグと同様の作りになっていますね。
NavLinkタグにはisActiveというプロパティが事前に用意されていて、現在いるページのメニュー名の色を以下のように書くことで変えることができます。
import React from 'react';
// ここを追加
import { NavLink } from 'react-router-dom';
const Top = () => {
return (
<div>
<ul>
{/* ここを変更 */}
<NavLink
to="/"
style={({ isActive }) => {
return isActive ? { color: 'red' } : {};
}}
>
<li>トップ</li>
</NavLink>
<NavLink to="/contact">
<li>お問い合わせ</li>
</NavLink>
</ul>
</div>
);
};
export default Top;
現在いるページのメニュー名の色が変わるようなデザインはWeb制作でよくありますよね。
CSSを書いても良いのですがisActiveとすればそのままカラーを指定できるのは便利です。
Reactでリダイレクトを作る方法
続いてNotFound.jsに移動してuseNavigateを使ったリダイレクトをやってみます。
NavigateというコンポーネントとuseNavigateというフックをReactRouterDomからインポートして以下のように書きます。
import React, { useEffect } from 'react';
// ここを追加
import { Navigate, useNavigate } from 'react-router-dom';
const NotFound = () => {
// ここを追加
const navigate = useNavigate();
return (
<div>
{/* ここを追加 */}
<NavLink to="/" />
NotFound
</div>
);
};
export default NotFound;
URLで適当に存在しない英語を入力してEnterキーを押すと自動的にTop.jsに返されるのがわかります。
また時間差でリダイレクトさせるにはNotFound.jsの中でsetTimeoutを実行すればOKです。
NavLinkコンポーネントは削除して、setTimeoutはuseEffectの初回実行の制約の中で書いてみます。
import React, { useEffect } from 'react';
// ここを追加
import { Navigate, useNavigate } from 'react-router-dom';
const NotFound = () => {
const navigate = useNavigate();
// ここを追加
useEffect(() => {
setTimeout(() => {
navigate('/');
}, 3000);
}, []);
return (
<div>
{/* NavLinkを削除しておく */}
NotFound
</div>
);
};
export default NotFound;
一旦はNotFound.jsに移動したことがわかり3秒後にTop.jsに戻ってきていますね。
こちらはuseNavigateの引数に入れたパスにリダイレクトできる方法なのですがsetTimeoutの中で実行することで時間差を作ることができています。
useNavigateの引数は前章でやった「-1」にして一つ前の画面に戻っても良いでしょう。
import React, { useEffect } from 'react';
// ここを追加
import { Navigate, useNavigate } from 'react-router-dom';
const NotFound = () => {
const navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
// ここを変更
navigate(-1);
}, 3000);
}, []);
return (
<div>
{/* NavLinkを削除しておく */}
NotFound
</div>
);
};
export default NotFound;
Reactでフォームの値をページ遷移で送る方法
続いてContact.jsに移動します。
ここではお問い合わせフォームをイメージして入力欄に入力した値を画面遷移と一緒に移動させる方法をやってみます。
useSearchParamsというフックを使うとinputタグの中のデータを取得することができます。
import React from 'react';
// ここを追加
import { NavLink, useNavigate, useSearchParams } from 'react-router-dom';
const Contact = () => {
// ここから追加
const [params, setParams] = useSearchParams({ q: '' });
const text = params.get('q');
console.log(text);
return (
<div>
Contact
<br />
<ul>
<NavLink>
<li>トップ</li>
</NavLink>
<NavLink to="/contact">
<li>お問い合わせ</li>
</NavLink>
</ul>
<button onClick={() => navigate(-1)}>戻る</button>
{/* ここから追加 */}
<br />
<input
type="text"
value={text}
onChange={(e) => setParams({ q: e.target.value })}
/>
<button>
送信
</button>
</div>
);
};
export default Contact;
入力した値がコンソールに表示されていますね。
useSearchParamsで入力された値の取得ができましたので後は遷移先に送るだけです。
データを別ページに遷移しながら送るときもuseNavigateが使用できて、第一引数には遷移先のパスを入れますが第二引数にはオブジェクト形式でデータを追加できます。
import React from 'react';
// ここを追加
import { NavLink, useNavigate, useSearchParams } from 'react-router-dom';
const Contact = () => {
const [params, setParams] = useSearchParams({ q: '' });
const text = params.get('q');
// ここを追加
const navigate = useNavigate();
return (
<div>
Contact
<br />
<ul>
<NavLink>
<li>トップ</li>
</NavLink>
<NavLink to="/contact">
<li>お問い合わせ</li>
</NavLink>
</ul>
<button onClick={() => navigate(-1)}>戻る</button>
<br />
<input
type="text"
value={text}
onChange={(e) => setParams({ q: e.target.value })}
/>
{/* ここから追加 */}
<button onClick={() =>
navigate('/', {
state: {
text,
},
})
}>
送信
</button>
</div>
);
};
export default Contact;
今回はTop.jsに送っていて、Top.jsではuseLocationというフックを使用することでデータを受け取って使用できるようになります。
import React from 'react';
// ここを追加
import { NavLink, useLocation } from 'react-router-dom';
const Top = () => {
// ここを追加
const location = useLocation();
console.log(location);
return (
<div>
<ul>
<NavLink
to="/"
style={({ isActive }) => {
return isActive ? { color: 'red' } : {};
}}
>
<li>トップ</li>
</NavLink>
<NavLink to="/contact">
<li>お問い合わせ</li>
</NavLink>
</ul>
</div>
);
};
export default Top;
Contact.jsで入力欄にテキストを入力して送信ボタンをクリックしてみます。
Top.jsに移動できたのでコンソールを確認してみるとオブジェクト形式の中に入力したテキストがあります。
useLocationはデータの引き受けに使用できて、オブジェクト形式になっているのでプロパティ名を指定すれば値を利用することができます。
useNavigateではstateというプロパティにデータを入れておきます。
そうすることでuseLocationでオブジェクト形式でデータを閲覧することができるようになります。
またuseNavigateでデータを送るのは複数でも可能です。
Contact.jsで変数descを新しく作成して文字列を追加で 送ってみます。
import React from 'react';
// ここを追加
import { NavLink, useNavigate, useSearchParams } from 'react-router-dom';
const Contact = () => {
const [params, setParams] = useSearchParams({ q: '' });
const text = params.get('q');
const navigate = useNavigate();
// ここを追加
const desc = '検索キーワードは';
return (
<div>
Contact
<br />
<ul>
<NavLink>
<li>トップ</li>
</NavLink>
<NavLink to="/contact">
<li>お問い合わせ</li>
</NavLink>
</ul>
<button onClick={() => navigate(-1)}>戻る</button>
<br />
<input
type="text"
value={text}
onChange={(e) => setParams({ q: e.target.value })}
/>
{/* ここを変更 */}
<button onClick={() =>
navigate('/', {
state: {
text,
desc
},
})
}>
送信
</button>
</div>
);
};
export default Contact;
もう一度入力欄にテキストを入力して送信ボタンをクリックしてTop.jsにやってきます。
コンソールのオブジェクトには2つの値があることが確認できました。
それではTop.jsの中でstateプロパティの中身を使って画面にテキストを表示させてみます。
import React from 'react';
import { NavLink, useLocation } from 'react-router-dom';
const Top = () => {
const location = useLocation();
console.log(location);
return (
<div>
<ul>
<NavLink
to="/"
style={({ isActive }) => {
return isActive ? { color: 'red' } : {};
}}
>
<li>トップ</li>
</NavLink>
<NavLink to="/contact">
<li>お問い合わせ</li>
</NavLink>
</ul>
{/* ここを追加 */}
{location.state.desc}{location.state.text}です。
</div>
);
};
export default Top;
useLocationで取得したデータをオブジェクトのプロパティ指定によって画面に表示することができました。
お問い合わせフォームなどで使えるテクニックですね。
JavaScriptだけでSPAの画面遷移を作る方法
余談としてJavaScriptのSPAがどのように作られているのか紹介します。
前章まで解説してきたReactRouterDomはサーバーにリクエストを送ってHTMLファイルをもらうWordPressのような形態とは異なります。
最初に全てのファイルをサーバーからもらっておき、ユーザーの動作に合わせてDOMを書き換えている仕組みです。
完全に同じではありませんがJavaScriptで似たようなことができるので作ってみます。
まずHTMLで全てのページの構成を書いておきます。
今回は「トップ」「サービス」「お問い合わせ」という3ページを作る想定で、それぞれmainタグの中でdivタグで作成します。
現状だと全てが画面に表示されてしまうので、class=”active”のセクションだけ表示して、残りの必要のないページはCSSで非表示にしておきます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./style.css" />
<script src="./script.js" defer></script>
</head>
<body>
<main>
<div class="page active" id="top">
<h1>トップ</h1>
<ul>
<li><a href="#" data-target="top" class="nav-link">トップ</a></li>
<li>
<a href="#" data-target="service" class="nav-link">サービス</a>
</li>
<li>
<a href="#" data-target="news" class="nav-link">お知らせ</a>
</li>
</ul>
</div>
<div class="page" id="service">
<h1>サービス</h1>
<ul>
<li><a href="#" data-target="top" class="nav-link">トップ</a></li>
<li>
<a href="#" data-target="service" class="nav-link">サービス</a>
</li>
<li>
<a href="#" data-target="news" class="nav-link">お知らせ</a>
</li>
</ul>
</div>
<div class="page" id="news">
<h1>お知らせ</h1>
<ul>
<li><a href="#" data-target="top" class="nav-link">トップ</a></li>
<li>
<a href="#" data-target="service" class="nav-link">サービス</a>
</li>
<li>
<a href="#" data-target="news" class="nav-link">お知らせ</a>
</li>
</ul>
</div>
</main>
</body>
</html>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.page {
display: none;
width: 100%;
min-height: 100%;
position: fixed;
padding: 8px 16px;
}
.active {
display: block;
}
#top {
background: #ff6868;
}
#service {
background: #ffeaa7;
}
#news {
background: #dcffb7;
}
初回のアクセスではトップページのセクションにclass=”active”をつけています。
画面にはメニューを作成しておりユーザーがクリックしたら現在のセクションが非表示になり、対象のセクションが表示になり「画面が切り替わる」という仕組みです。
ReactRouterDomも似たようなことをしており純粋な画面遷移をしているわけではないのです。
続いてクリックした時の画面の切り替えをJavaScriptで作ります。
const app = {
init: function () {
const links = document.querySelectorAll('.nav-link');
links.forEach((link) => {
// メニューをクリックして移動するイベント
link.addEventListener('click', app.nav);
});
},
nav: function (e) {
e.preventDefault();
// HTML,CSSでページ表示、非表示の変更
// data-targetを取得
let currentPage = e.target.getAttribute('data-target');
document.querySelector('.active').classList.remove('active');
document.getElementById(currentPage).classList.add('active');
},
};
document.addEventListener('DOMContentLoaded', app.init);
addEventLinstenerのclickイベントでクリックされたメニューを取得し照合します。
事前にHTML側でidとdata-targetを同じ値にしていたので、クリックしたメニュー項目のdata-taragetの値を取得できるので同時にidも取得できる仕組みです。
後はclass=”active”をつけたり外したりするだけです。
ただこれだけだとブラウザバックのようなセッション履歴の移動ができません。
セッション履歴についてはhistoryというクラスを使用します。
まずはhistory.replaceStateで初期のセクションをid=”top”のセクションに固定します。
history.replaceStateをリロード時のイベントに書くとそれまでの履歴を強制的に上書きできます。
それによりサイト訪問時にはトップを表示することになります。
const app = {
init: function () {
const links = document.querySelectorAll('.nav-link');
links.forEach((link) => {
link.addEventListener('click', app.nav);
});
// ここを追加
// 現在のセッション履歴を操作してリロード時には初期のURL('/index.html#top')にする
history.replaceState({}, 'Top', '#top');
},
nav: function (e) {
e.preventDefault();
let currentPage = e.target.getAttribute('data-target');
document.querySelector('.active').classList.remove('active');
document.getElementById(currentPage).classList.add('active');
},
};
document.addEventListener('DOMContentLoaded', app.init);
続いてブラウザバックのような仕組みはwindowのhashchangeというイベントで作ることができます。
const app = {
init: function () {
const links = document.querySelectorAll('.nav-link');
links.forEach((link) => {
link.addEventListener('click', app.nav);
});
history.replaceState({}, 'Top', '#top');
// ここを追加
// ブラウザの矢印を使って移動するイベント
window.addEventListener('hashchange', app.popState);
},
nav: function (e) {
e.preventDefault();
let currentPage = e.target.getAttribute('data-target');
document.querySelector('.active').classList.remove('active');
document.getElementById(currentPage).classList.add('active');
},
};
document.addEventListener('DOMContentLoaded', app.init);
ブラウザバックはユーザーが「一つ前にいたページを記憶する」ということから成り立っています。
セッションの追加とも呼ばれていて、ブラウザにセッションというユーザーの記憶を追加させることが以下のコードで可能になります。
const app = {
init: function () {
const links = document.querySelectorAll('.nav-link');
links.forEach((link) => {
link.addEventListener('click', app.nav);
});
history.replaceState({}, 'Top', '#top');
window.addEventListener('hashchange', app.popState);
},
nav: function (e) {
e.preventDefault();
let currentPage = e.target.getAttribute('data-target');
document.querySelector('.active').classList.remove('active');
document.getElementById(currentPage).classList.add('active');
// ここを追加
// ブラウザーのセッション履歴に現在のページのハッシュを追加
history.pushState({}, currentPage, `#${currentPage}`);
},
};
document.addEventListener('DOMContentLoaded', app.init);
変数currentPageにはクリックしたメニュー項目をdata-targetを目印に取得したid名が入っていました。
それを「https://〇〇.com/#top」のように「#」でつくアンカーリンクとして保存します。
「https://〇〇.com/#top」「https://〇〇.com/#service」「https://〇〇.com/#contact」といった具合にセクションごとに固有のURLを持つことでブラウザがページとして認識してくれるわけです。
後はブラウザバック時にもclass=”active”のつけたり外したりの処理を書いてあげるだけです。
const app = {
init: function () {
const links = document.querySelectorAll('.nav-link');
links.forEach((link) => {
link.addEventListener('click', app.nav);
});
history.replaceState({}, 'Top', '#top');
window.addEventListener('hashchange', app.popState);
},
nav: function (e) {
e.preventDefault();
let currentPage = e.target.getAttribute('data-target');
document.querySelector('.active').classList.remove('active');
document.getElementById(currentPage).classList.add('active');
history.pushState({}, currentPage, `#${currentPage}`);
},
// ここを追加
// ブラウザバックした時の処理
popState: function () {
let hash = location.hash.replace('#', '');
document.querySelector('.active').classList.remove('active');
document.getElementById(hash).classList.add('active');
},
};
document.addEventListener('DOMContentLoaded', app.init);
先ほどURLの#の続きにid名があるとわかっていますので、「#top」を「top」といった具合にreplaceメソッドでセクションのid名を取得します。
後は該当するid名のdivタグにclass=”active”を追加して、それ以外からはclass=”active”を外すわけです。
これでシングルページアプリケーションという仕組みができました。
ユーザーからは画面が切り替わっているように見えてますが、実際にはサーバーにリクエストしているわけではないので初回アクセス以降はページの移動がスムーズに見えますね。
これが近年SPAがトレンドになっている理由です。
実際にみなさんがこれを作る必要はなく前章までに紹介したReactRouterDomを使うだけですが、裏側がどうなっているかは知っておいて損はないでしょう。