66
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ateam LifeDesignAdvent Calendar 2022

Day 8

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

Last updated at Posted at 2022-12-07

この記事は、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-domreact-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>

数字を変更してもNptfoundが出ません。

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

スクショ13.png

今のままだと、記事のリストと記事内容が同時に表示されています。
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だとブックマークがたくさんあるので、見られるのが恥ずかしいためです(笑)

66
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
66
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?