2022年中にreact-router-domの理解を深めたい

この記事は、[Ateam LifeDesign Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ateam-life-design) シリーズ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
```

```jsx
//上記コマンドにて、私はこのようなバージョンで作成されました。
 "react": "^18.2.0",
 "react-dom": "^18.2.0",
 "react-router-dom": "6",
```

まずはApp.jsの内容を簡単にシンプルに修正しておきます。

```jsx
// App.js 色々消して、これだけにしておく。
function App () {
  return (
    <div className="App">
      <h1>
        react-router-V6
      </h1>
    </div>
  );
}

export default App;
```

続いて`routesディレクトリ`を作成し、3つほどコンポーネントを用意しておきます。

```jsx
// src/routes/Home.js
export const Home = () =>{
    return (
        <div>
          <p>Home</p>
        <div>
    )
}
```
```jsx
// src/routes/About.js
export const About = () =>{
    return (
        <div>
          <p>About</p>
        <div>
    )
}
```
```jsx
// src/routes/Contact.js
export const Contact = () =>{
    return (
        <div>
          <p>Contact</p>
        <div>
    )
}
```
いったん表示確認をしておきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/7be00706-3be1-6668-5d77-46dd224085f6.png" width="300">

正しくコンポーネントが表示されていした。
準備が整ったので、ルーティングの設定を行います。

## ルーティングしていく
`src/index.jsファイル`に変更を加えていきます。 `react-router-dom`をimportし、`BrowserRouter`を設定します。

```jsx
// 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コンポーネント` を表示するようにします。

```jsx
// 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;
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/15d189a1-78f1-d189-2f37-2df8b81c87dc.png" width="300">

`/ のルート` に対して、`Homeコンポーネントを表示させる`ことができました。
試しに、まだルートの設定していない `/about` にアクセスしてみると、App.jsの内容だけが残り<b>Homeの内容が消えるはず</b>です。

続いて残りのルーティングも行います。 `Routes の中に Route を増やしていくだけ`でOKです。

```jsx
// 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コンポーネントの内容が表示されている事を確認できました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/aad9c730-19e4-d919-e0a3-18c12f7136e5.png" width="300">

## NotFoundを設定する
指定していないルートには、`Notfound用に作成したコンポーネントを出しておきたいです`。
(ちなみにNotfoundページにも、[サイトに戻る]など次の行動を促す動線があるといいですね。)
それも簡単に出すことができます。

```jsx
// 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コンポーネント`が表示されます

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/7202e03f-dcd4-4bd6-f99e-0f645074d00f.png" width="300">

## 子コンポーネントにpropsを渡す
Route に設定したコンポーネントに`propsを持たせる`のはこのように実装します。

```jsx
// 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）は分割代入で渡すとわかりやすい](https://dezanari.com/react-component-props-object/)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/1c87cadf-afc1-e367-0380-56c89287d9d8.png" width="300">

## リンクを作成し追加する

今まで実装したルートを`フッターにリンクとして設置`すると確認が捗るので設置します。
`Footerコンポーネント`にlistを作っていきます。
ポイントは `<Link> コンポーネントを使用すること`です。

```jsx
// 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>
    )
}
```

```jsx
// 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 /> {/*// ひとまずここに追加*/}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/b16c6912-f560-1d92-4415-b4f5a337bfd1.png" width="300">


リンクを押すと、`コンポーネントの中身が切り替わっている`のが分かります。
ちなみに、`Linkコンポーネント`を使わずに `<a href="/about">about</a>` とすると、`ページがリロード`してしまいました。

:::note info
ちなみに`NavLink`を使用すると、現在アクセスしているリンクだけ色を変えるなどできますので、こちらも試しておくのがよいです。

```jsx
// Footer.jsに追加
<NavLink style={({ active }) => (active ? { color: 'red' } : undefined)} to="/about">
```
:::

### useNavigate
buttonを押して他のページにジャンプしたい時は、このhookを使用するのがよさそう。
(aタグをボタンっぽくcss書けばいいのであんまりないかな？)

```jsx
// 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` を試してみます。

```jsx
// Post.jsを作成
export const Post = () => {
    return (
        <div>
            <p>Post</p>
        </div>
    )
}
```
```jsx
// 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>
```
```jsx
// Footer.js リストに1行追加
<li><Link to="/posts">Posts</Link></li> // 追加
```

### post/postページの作成
続いてネストされた記事ページを作成します。

```jsx
// Post.jsを作成
export const Post = () => {
    return (
        <div>
            <h2>Postのページ</h2>
        </div>
    )
}
```

`posts`のルーティングをしたRouteコンポーネントに、`post`のルーティングを追加します。

```jsx
// App.js
// 下記の形に変更
<Route path="/posts" element={<Posts />}>
  <Route path="post" element={<Post />} /> {/* /(スラッシュ)無しでもOK*/}
</Route>
```

あれ、、、
/posts/post の画面をみると、`まだPostコンポーネントが表示されていない`ようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/1ffe1abf-70c2-831b-76c4-5dd8885f135a.png" width="300">


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

```jsx
// Posts.js
import { Outlet } from 'react-router-dom'; //追加

export const Posts = () => {
    return (
        <>
            <h2>Postsのページ</h2>
            <Outlet /> // 追加
        </>
    );
}
```

ネストされた Post が表示されているのを確認できました。 

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/c6fa6337-ea31-061a-a5ed-1bdb8d7e77e3.png" width="300">


## idによる表示切り替え
続いて、`/posts/1`,`/posts/2` のように、`記事idで表示を切り替える`方法を試します。 
今のままで `/posts/1` にアクセスすると`Notfound`が出てしまいます。
pathの設定値に`:`をつけることで/posts/1, /posts/2,でも`postコンポーネントの内容が表示される`ようになります。

```jsx
//App.js
<Route path="/posts" element={ <Posts /> }>
    <Route path=":postId" element={ <Post /> } /> {/*変更*/}
</Route>
```

数字を変更してもNptfoundが出ません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/e40e59de-0a4d-bde8-33c4-4cae7beb08e3.png" width="300">


### useParams 
useParams Hookを使い、urlに含まれる末尾の数字部分を取得してみましょう。

```jsx
//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" の部分` ですね

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/e9f0e279-cf7d-50f5-f2aa-43558ed945e0.png" width="300">

あとはこの postId を使用してAPIを叩けば表示を切り替えることができそうですワクワク。 
早速実装してみます。

```jsx
// 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によって表示を切り替えることができました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/bc851fec-a563-ef9e-9cfb-2fc9a786da7a.png" width="300">


## 記事の一覧ページを作る
Postsにて、記事一覧を表示してみます。 
一覧にはLinkを設置し、Linkを押したら `post/5` など記事の詳細が出るようにする魂胆です。

```jsx
// 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 />
        </>
    );
}
```

![スクショ13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/bb7351ee-ddda-18ff-7acd-626f60720509.png)


今のままだと、記事のリストと記事内容が同時に表示されています。 
postsは`index扱いにして`、分離してみます。

```jsx
// 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に処理を移動したので、スッキリしました。

```jsx
// Posts.js 処理を移動し、これだけになった
import { Outlet } from 'react-router-dom';
export const Posts = () => {
    return (
        <>
            <h2>Postsのページ</h2>
            <Outlet />
        </>
    );
}
```

```jsx
// App.js
<Route path="/posts" element={<Posts />}>
  <Route index element={<PostIndex />} /> {/*追加して動かす*/}
  <Route path=":postId" element={<Post />} />
</Route>
```

postsページとpostページで、内容を切り替えることができました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129861/982cdb96-d5fe-b570-c281-60bd611eeccd.png" width="300">


## おわりに
実装しながら確認すると理解が進みました。
2022年中に行う事ができてスッキリです！
便利なHooksもたくさんあるので、こちらも試しておく必要はありますね。[ReactRouterの公式はこちら](https://reactrouter.com/en/main/start/tutorial)
今年もまだあるし、やり残し無いようアウトプットしていこう！！


普段はchromeで試しているのですが、今回はfirefoxでやってみました。
chromeだとブックマークがたくさんあるので、見られるのが`恥ずかしいため`です(笑)