この記事は、Ateam LifeDesign Advent Calendar 2022 シリーズ1の8日目の記事です。
はじめに
react-router-domについて、ふわっと理解していた部分がありました。
このまま年を越すには、どうも気持ちが落ち着かない。。。
というわけこの際、1から全部実装して動作を確認していこうと思います。
React Routerって何?
Reactで作成したSPAに、UIとURLを紐づけるためのもの
です。
http://localhost:3000/top
にアクセス -> Topコンポーネントを返す
http://localhost:3000/todo
にアクセス -> Todoコンポーネントを返す
といった事ができます。
今回やること
今回はルーティングを確認したいので、react-router-dom
を使用します。
react-router-dom
はreact-router
の上位互換のようなものですので、基本的にはreact-router-dom
を使用するでOKです。
環境構築
まずはReactの環境構築を行います。
コマンドを叩く
npx create-react-app testV6
cd testV6
yarn add react-router-dom@6
//上記コマンドにて、私はこのようなバージョンで作成されました。
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "6",
まずはApp.jsの内容を簡単にシンプルに修正しておきます。
// App.js 色々消して、これだけにしておく。
function App () {
return (
<div className="App">
<h1>
react-router-V6
</h1>
</div>
);
}
export default App;
続いてroutesディレクトリ
を作成し、3つほどコンポーネントを用意しておきます。
// src/routes/Home.js
export const Home = () =>{
return (
<div>
<p>Home</p>
<div>
)
}
// src/routes/About.js
export const About = () =>{
return (
<div>
<p>About</p>
<div>
)
}
// src/routes/Contact.js
export const Contact = () =>{
return (
<div>
<p>Contact</p>
<div>
)
}
いったん表示確認をしておきます。
正しくコンポーネントが表示されていした。
準備が整ったので、ルーティングの設定を行います。
ルーティングしていく
src/index.jsファイル
に変更を加えていきます。 react-router-dom
をimportし、BrowserRouter
を設定します。
// src/index.js
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(
<BrowserRouter> {/*BrowserRouterで囲む*/}
<App />
</BrowserRouter>
);
App.jsファイルに戻り、/
にアクセスがあった場合は、
Homeコンポーネント
を表示するようにします。
// App.js
import { Routes, Route } from "react-router-dom"; // 追加
import { Home } from "./routes/Home" // Homeだけimport
function App () {
return (
<div className="App">
<h1>React-router-V6</h1>
<Routes> {/*Routesで囲む*/}
<Route path="/" element={ <Home /> } /> {/*RouteにHomeを設定する*/}
</Routes>
</div>
);
}
export default App;
/ のルート
に対して、Homeコンポーネントを表示させる
ことができました。
試しに、まだルートの設定していない /about
にアクセスしてみると、App.jsの内容だけが残りHomeの内容が消えるはずです。
続いて残りのルーティングも行います。 Routes の中に Route を増やしていくだけ
でOKです。
// App.js
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>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コンポーネントの内容が表示されている事を確認できました。
NotFoundを設定する
指定していないルートには、Notfound用に作成したコンポーネントを出しておきたいです
。
(ちなみにNotfoundページにも、[サイトに戻る]など次の行動を促す動線があるといいですね。)
それも簡単に出すことができます。
// App.js
import { Notfound } from "./routes/Notfound"; // 追加
// ~~略~~
<Routes>
<Route path="/" element={ <Home /> } />
<Route path="/about" element={ <About /> } />
<Route path="/contact" element={ <Contact /> } />
<Route path="*" element={ <Notfound /> } /> {/*追加*/}
</Routes>
これにより、存在しないパスでアクセスすれば下記のようにNotfoundコンポーネント
が表示されます
子コンポーネントにpropsを渡す
Route に設定したコンポーネントにpropsを持たせる
のはこのように実装します。
// App.js
<Route path="/contact" element={ <Contact message="Hello" /> } /> // messageを渡す
// Contact.js
export const Contact = ( { message } ) => { // 分割代入で受け取る
return (
<div>
<p>Contactです、{ message }</p>
</div>
)
}
参考 : 【React】関数コンポーネントの引数(props)は分割代入で渡すとわかりやすい
リンクを作成し追加する
今まで実装したルートをフッターにリンクとして設置
すると確認が捗るので設置します。
Footerコンポーネント
にlistを作っていきます。
ポイントは <Link> コンポーネントを使用すること
です。
// Footer.jsを作成
import { Link } from "react-router-dom"; // 追加
export const Footer = () => {
return (
<ul>
<li>
<Link to="/">Home</Link> {/*aタグじゃないよ*/}
</li>
<li>
<Link to="/about">about</Link>
</li>
<li>
<Link to="/contact">contact</Link>
</li>
</ul>
)
}
// App.js
import { Footer } from "./routes/Footer" // 追加
// ~略~
<Routes>
<Route path="/" element={ <Home /> } />
<Route path="/about" element={ <About /> } />
<Route path="/contact" element={ <Contact message="Hello" /> } />
<Route path="*" element={ <Notfound /> } />
</Routes>
<Footer /> {/*// ひとまずここに追加*/}
リンクを押すと、コンポーネントの中身が切り替わっている
のが分かります。
ちなみに、Linkコンポーネント
を使わずに <a href="/about">about</a>
とすると、ページがリロード
してしまいました。
ちなみにNavLink
を使用すると、現在アクセスしているリンクだけ色を変えるなどできますので、こちらも試しておくのがよいです。
// Footer.jsに追加
<NavLink style={({ active }) => (active ? { color: 'red' } : undefined)} to="/about">
useNavigate
buttonを押して他のページにジャンプしたい時は、このhookを使用するのがよさそう。
(aタグをボタンっぽくcss書けばいいのであんまりないかな?)
// Contact.js
import { useNavigate } from 'react-router-dom'; //追加
export const Contact = ( { message } ) => {
const navigate = useNavigate();
return (
<div>
<p>Contactです、{ message }</p>
<button onClick={() => navigate('/about')}>about</button> {/*onClickで遷移*/}
</div>
)
}
ルーティングのネスト化
posts/1
,posts/2
のように、投稿した記事ごとに表示を切り替えることができます。
記事固有のidを割り振り
、それを用いて表示を切り替えてみます。
まずは posts/post
を試してみます。
// Post.jsを作成
export const Post = () => {
return (
<div>
<p>Post</p>
</div>
)
}
// App.js Routesの中だけ修正
<Routes>
<Route path="/" element={ <Home /> } />
<Route path="/about" element={ <About /> } />
<Route path="/contact" element={ <Contact message="Hello" /> } />
<Route path="/posts" element={ <Posts /> } /> {/*追加*/}
<Route path="*" element={ <Notfound /> } />
</Routes>
// Footer.js リストに1行追加
<li><Link to="/posts">Posts</Link></li> // 追加
post/postページの作成
続いてネストされた記事ページを作成します。
// Post.jsを作成
export const Post = () => {
return (
<div>
<h2>Postのページ</h2>
</div>
)
}
posts
のルーティングをしたRouteコンポーネントに、post
のルーティングを追加します。
// App.js
// 下記の形に変更
<Route path="/posts" element={<Posts />}>
<Route path="post" element={<Post />} /> {/* /(スラッシュ)無しでもOK*/}
</Route>
あれ、、、
/posts/post の画面をみると、まだPostコンポーネントが表示されていない
ようです。
Postコンポーネントの内容を表示させるには
OutletコンポーネントをPostsコンポーネントで設定する必要がありますので設定します。
// Posts.js
import { Outlet } from 'react-router-dom'; //追加
export const Posts = () => {
return (
<>
<h2>Postsのページ</h2>
<Outlet /> // 追加
</>
);
}
ネストされた Post が表示されているのを確認できました。
idによる表示切り替え
続いて、/posts/1
,/posts/2
のように、記事idで表示を切り替える
方法を試します。
今のままで /posts/1
にアクセスするとNotfound
が出てしまいます。
pathの設定値に:
をつけることで/posts/1, /posts/2,でもpostコンポーネントの内容が表示される
ようになります。
//App.js
<Route path="/posts" element={ <Posts /> }>
<Route path=":postId" element={ <Post /> } /> {/*変更*/}
</Route>
useParams
useParams Hookを使い、urlに含まれる末尾の数字部分を取得してみましょう。
//Post.js
import { useParams } from 'react-router-dom'; //追加
export const Post = () => {
const params = useParams(); //追加
console.log( params ); //確認のため追加
return (
<div>
<h2>Postのページ</h2>
</div>
)
}
postIdという名前で、数字が取得できるようです。
postIdは、先ほどApp.jsで実装した path=":postId" の部分
ですね
あとはこの postId を使用してAPIを叩けば表示を切り替えることができそうですワクワク。
早速実装してみます。
// Post.js
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
export const Post = () => {
const { postId } = useParams(); // postIdの数字取得
const [ post, setPost ] = useState(''); // postには非同期で取得したの内容が入る
useEffect( () => {
// useEffect内で関数の定義
const fetchPost = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${ postId }` // ここでpostIdを使用する
);
const data = await res.json();
setPost( data ); // postで使えるようにセットする
};
fetchPost(); // 関数起動
}, [ postId ] ); // useeEffectはpostIdに依存させる
return (
<div>
<h2>記事のページ</h2>
<div>
<p>postId:{ post.id }</p>
<p>タイトル:{ post.title }</p>
<p>body:{ post.body }</p>
</div>
</div>
)
}
無事postIdによって表示を切り替えることができました。
記事の一覧ページを作る
Postsにて、記事一覧を表示してみます。
一覧にはLinkを設置し、Linkを押したら post/5
など記事の詳細が出るようにする魂胆です。
// Posts.js
import { useEffect, useState } from 'react';
import { Link, Outlet } from 'react-router-dom';
export const Posts = () => {
const [ posts, setPosts ] = useState( [] ); // 配列を受け取る想定
useEffect( () => {
const fetchPosts = async () => {
const res = await fetch( 'https://jsonplaceholder.typicode.com/posts' ); // postIdなし
const data = await res.json();
setPosts( data ); // 受け取った配列をセット
};
fetchPosts();
}, [] ); // 空にして、訪問時に1度だけ動かします
return (
<>
<h2>Postsのページ</h2>
<ul>
{ posts.map( ( { id, title } ) => ( // mapで回します
<li key={ id }>
<Link to={ `/posts/${ id }` }>
{ id }:{ title }
</Link>
</li>
) ) }
</ul>
<Outlet />
</>
);
}
今のままだと、記事のリストと記事内容が同時に表示されています。
postsはindex扱いにして
、分離してみます。
// PostIndex.jsを作成 Posts.jsから不要なものを取り除いただけです。
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
export const PostIndex = () => {
// カスタムhooksにして外へ逃したい処理
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( ( { id, title } ) => (
<li key={ id }>
<Link to={ `${ id }` }> {/*idだけでOK*/}
{ id }:{ title }
</Link>
</li>
) ) }
</ul>
);
}
PostIndexに処理を移動したので、スッキリしました。
// Posts.js 処理を移動し、これだけになった
import { Outlet } from 'react-router-dom';
export const Posts = () => {
return (
<>
<h2>Postsのページ</h2>
<Outlet />
</>
);
}
// App.js
<Route path="/posts" element={<Posts />}>
<Route index element={<PostIndex />} /> {/*追加して動かす*/}
<Route path=":postId" element={<Post />} />
</Route>
postsページとpostページで、内容を切り替えることができました。
おわりに
実装しながら確認すると理解が進みました。
2022年中に行う事ができてスッキリです!
便利なHooksもたくさんあるので、こちらも試しておく必要はありますね。ReactRouterの公式はこちら
今年もまだあるし、やり残し無いようアウトプットしていこう!!
普段はchromeで試しているのですが、今回はfirefoxでやってみました。
chromeだとブックマークがたくさんあるので、見られるのが恥ずかしいため
です(笑)