welcome to ginnan blog.

Ginnan blog

銀杏ブログへようこそ。
主にプログラミングの学習アウトプットです。

Router

目次

React Routerとは

通常のwebページはページ遷移するとHTTP通信をして別ページのHTMLをもらって表示する方式だとURLとページが紐ついているのでリロードをしてもちゃんと遷移後のページが表示されます。

しかしReact等で作成するSPAは単純にJavaScriptでDOMを操作するだけだと画面操作でUIが変わってもリロードするとトップページに戻ってしまいます。

そのため従来のページのようにUIが変わればそれに応じでURLも動的に変更する必要があります。
それを実現するのがReact Routerになります。

Routerのインストール

  
% npm install react-router-dom
  
package.json

{ "name": "react-router", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "web-vitals": "^1.0.1" }, // ~略~ }

Router最小構成

routerを活用した際の最小の画面遷移のSPAを作成します。

コードとしては以下となります。
Home,Page1,Page2は事前に用意したコンポーネントになります。中身はh1タグだけのシンプルなものです。


import { BrowserRouter, Link, Switch, Route } from "react-router-dom";
import { Home } from "./Home";
import { Page1 } from "./Page1";
import { Page2 } from "./Page2";

export default function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Link to="/">▼Home </Link>
        <Link to="/page1">▼Page1 </Link>
        <Link to="/page2">▼Page2 </Link>
      </div>
      <Switch>
        <Route exact path="/"> <Home /> </Route>
        <Route path="/page1"> <Page1 /> </Route>
        <Route path="/page2"> <Page2 /> </Route>
      </Switch>
    </BrowserRouter>
  );
}

実際に表示される画面

それぞれのコンポーネントの説明を簡単にします。

< BrowserRouter >

SPAを構成するためのreact-router-domのコンポーネントをラッピングするためのコンポーネントです。BrowserRouterで囲まないと以下のようなエラーが発生します。

< Link >< Router >の外では使えないと言われています。
実はBrowserRouterはRouterというコンポーネントのwrapperとなっているのでBrowserRouterで< Link >< Switch >などを囲んでおけばOKです。

またreact-router-domと似たライブラリでreact-routerなるものがあってそちらでは< Router >を活用するらしいです。

< Link >

< Link >はaタグの役割を担います。
hrefタグに入れる値をtoというpropsに渡します。

< Switch >

< Switch >URLが変わることで表示を切り替える範囲を囲むものです。

< Switch >で囲んだ中で更に詳細にURL毎の表示を< Route >で決定します

< Route >

< Route >はpathというpropsにURIを設定します。
そうすることでそのURIに一致した時に< Route >で囲まれたコンポーネントが表示されます。

render

今までは< Route >で表示するコンポーネントを囲んでいたのですがrenderプロパティを使うことでも表示するコンポーネントを選択できます。


// Routeで囲む場合
      <Switch>
        <Route exact path="/"> <Home /> </Route>
        <Route path="/page1"> <Page1 /> </Route>
        <Route path="/page2"> <Page2 /> </Route>
      </Switch>


// renderを活用した場合
    <Switch>
      <Route exact path="/" render={() => <Home />} />
      <Route path="/page1" render={() => <Page1 />} />
      <Route path="/page2" render={() => <Page2 />} />
    </Switch>

またexactオプションはpathに渡されたURIと完全一致するときのみ表示するというオプションになります。基本的にはpathに渡されたURIは部分一致になるので/に対して部分一致にしてしまうと全てのURLに一致してしまうのでその< Route >の要素しか表示されなくなってしまいます。

routerのネスト

結論、routerのネストといっても特にやることは変わりません。
更にPage1の中に表示するSection1とSection2を用意しました。
< Switch >の中に更に< Switch >を用意することでrouterのネストは実現できます。

本来はネストする場合などはファイルを切り分けますが見やすさのために1つのファイルで表現しています。

ハイライト部分が追加したrouterになります。


import { BrowserRouter, Link } from "react-router-dom";
import { Page1 } from "./Page1";
import { Page2 } from "./Page2";
import { Section1 } from "./Page1Section1";
import { Section2 } from "./Page1Section2";

export default function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Link to="/">▼Home </Link> <Link to="/page1">▼Page1 </Link> <Link to="/page2">▼Page2 </Link>
      </div>
      <Switch>
        <Route exact path="/" render={() => <Home />} />
        <Route path="/page1" render={() => (
            <Switch>
              <Route exact path="/page1" render={() => <Page1 />} />
              <Route path="/page1/section1" render={() => (
                  <div> <Page1 /> <Section1 /> </div>
                )}/>
              <Route path="/page1/section2" render={() => (
                  <div> <Page1 /> <Section2 /> </div>
                )}/>
            </Switch>
          )}
        />
        <Route path="/page2" render={() => <Page2 />} />
      </Switch>
    </BrowserRouter>
  );
}


renderのprops

上記のコードでもrouterのネストは実現できていますが、ネストの階層が増えて複雑になるとURLの指定などでtypoなどが起きる可能性があります。

そこでrendreのpropsを使うことでURLを変数で置き換えることが望ましいです。

propsの中身をconsole.logで見てみます。


      <Route
        path="/page1"
        render={(props) => (
          <Switch>
            {console.log(props)}

propsの中にはhistory,location,match,staticContextの4つがあります。
matchのurlなどを活用することでURLを変数で表現できそうです。

console.logの結果

history: {length: 50, action: "PUSH", location: {…}, createHref: ƒ, push: ƒ, …} location: {pathname: "/page1", search: "", hash: "", state: undefined, key: "y1yqpl"} match: {path: "/page1", url: "/page1", isExact: true, params: {…}} staticContext: undefined

propsを活用すると以下のようなコードになります。


// 変更前
render={() => (
    <Switch>
      <Route exact path="/page1" render={() => <Page1 />} />
      <Route path="/page1/section1" render={() => (
          <div> <Page1 /> <Section1 /> </div>
      )}
    />

// 変更後
render={({ match: { url } }) => (
  <Switch>
    <Route exact path={url} render={() => <Page1 />} />
    <Route path={`${url}/section1`} render={() => (
        <div> <Page1 /> <Section1 /> </div>
      )}
    />

renderをループで処理

renderをネストしており並列の要素も多い場合にはmapメソッドなどでのループ処理で実装するのが望ましいです。

今回はpage1の中に更にsection1とsection2のrouterを用意して表示したいと思います。

ネストの構造としては以下のようになります。

Router.jsx内でのrouterの構造

Router.jsx └ / └  /page1 └ page1Routes.jsxをループ └ /page1 └ /page1/section1 └ /page1/section2 └ /page2

実際のファイルでは以下のような構造になります。

Router.jsx

<Switch> <Route exact path="/" render={() => <Home />} /> <Route path="/page1" render={({ match: { url } }) => ( <Switch> {page1Routes.map((route) => ( <Route key={route.path} exact={route.exact} path={`${url}${route.path}`} > {route.children} </Route> ))} </Switch> )} /> <Route path="/page2" render={() => <Page2 />} /> </Switch>
PageRouter.jsx

import { Page1 } from "../Page1"; import { Section1 } from "../Page1Section1"; import { Section2 } from "../Page1Section2"; export const page1Routes = [ { path: "/", exact: true, children: <Page1 />, }, { path: "/section1", exact: false, children: <Section1 />, }, { path: "/section2", exact: false, children: <Section2 />, }, ];

またここでの注意点としてはmapメソッドでループを回している要素にkeyプロパティを渡していることです。mapメソッドではkeyという一意の情報を渡すことでどの要素が変更、追加もしくは削除されたのかを React が識別するのに役立ちます。配列内の項目に安定した識別性を与えることができます。

今回はroute.pathで一意性が保たれています。

URLパラメータ

URLパラメータの設定方法

今回はidというパラメータを設定します。(パラメータ名は任意です。)


// Route側
<Route path="/:id" />
// :(コロン)の後に任意のパラメータ名

// Link側
<Link to="/page2/100"></Link>
// URLに直接パラメータを渡す。(基本的にはJavaScritpで変数をもたせる)

URLパラメータの受け取り

idという名前でパラメータを設定したのでidという名称でパラメータを受け取ります。
react-routerのuseParamsを活用することで取得できます。


import { useParams } from "react-router";
export const UrlParameter = () => {
  const { id } = useParams();
  return (
    <>
      <h2>This is UrlParameter.</h2>
      <p>parameter is {id}</p>
    </>
  );
};

URL上に正しくパラメータが渡せています。

画面上にも正しくパラメータが表示できています。

クエリパラメータ

クエリパラメータはURL上でkey,value形式でパラメータを送信することができる形式になります。

クエリパラメータの設定方法

設定方法は?パラメータ名=値です。


<Link to="/page2/100?name=ginnan"></Link>

正しくURLにクエリパラメータを渡すことができています。

クエリパラメータの受け取り方

こちらもURLパラメータのときと同じくreact-routerの中にあるメソッドで受け取りが可能です。useLocationというメソッドを活用します。


import {  useLocation } from "react-router";
export const UrlParameter = () => {
  const location = useLocation();
  console.log(location);
  return (
    <>
      <h2>This is UrlParameter.</h2>
    </>
  );
};

useLocation()で取得できる値をconsole.logで確認してみます

console.log

{pathname: "/page2/100", search: "?name=ginnan", hash: "", state: undefined, key: "jr0a58"}

searchの中にクエリパラメータと値が入っています。
こちらを活用すれば良さそうですがこのままではデータとして活用しづらいです。
そこでこのsearchを扱いやすくしてくれるURLSearchParamsというものを活用します。


import {  useLocation } from "react-router";
export const UrlParameter = () => {
  const { search } = useLocation();
  const query = new URLSearchParams(search);
  return (
    <>
      <h2>This is UrlParameter.</h2>
      <p>query parameter is {query.get("name")}</p>
    </>
  );
};

URLSearchParamsに引数としてsearchを渡して上げることで便利なメソッドが活用できるようになります。
クエリパラメータはquery.get("クエリパラメータ名")で取得することができます。

stateの受け渡し

stateの値をページ遷移した先のページに受け渡すことも可能です。
このときのstateはLinkコンポーネントのtoにオブジェクトとして渡します。

toの書き方をobjectにできるというのは公式のドキュメントにも記載があります。

react公式(to: object)

今回は画面遷移前のページでLinkにstateValueという変数で’hello world’という文字列を渡しておきます。


import { Link } from "react-router-dom";
export const Page1 = () => {
  const stateValue = "hello world";
  return (
    <>
      <h1>This is page one.</h1>
      <Link to={{ pathname: "/page1/section1", state: stateValue }}>
        Sction1
      </Link>
    </>
  );
};

遷移後のページで取得できるか確認します。
stateの値はクエリパラメータの時と同様にuseLocationで取得することができます。


import { useLocation } from "react-router";
export const Section1 = () => {
  const { state } = useLocation();
  console.log(state);
  return (
    <>
      <h2>This is Sciton 1.</h2>
    </>
  );
};
// => hello world

hello worldという文字列を渡せていることが確認できました。

Linkを使わずに画面遷移する方法

reactはLinkを使わなくても画面遷移することができます。
結論、onClickでボタンをクリックした際の挙動をLinkコンポーネントの挙動と同じようにすることができます。


import { Link, useHistory } from "react-router-dom";
export const Page1 = () => {
  const history = useHistory();
  const onClickSection = () => history.push("/page1/section1");
  return (
    <>
      <h1>This is page one.</h1>
      <button onClick={onClickSection}>Section1</button>
    </>
  );
};

useHistoryというメソッドを活用することで実現することができます。
history.pushの引数にLinkのtoに渡すリンクと同じものを渡すことで実現できます。

Backボタンの実装

結論、history.goBackというメソッドで実現することができます。


import { useHistory } from "react-router";
export const Section1 = () => {
  const history = useHistory();
  const onClickBack = () => history.goBack();
  return (
    <>
      <h2>This is Sciton 1.</h2>
      <button onClick={onClickBack}>Back</button>
    </>
  );
};

404ページの作成

まずは404ページを用意します。


import { Link } from "react-router-dom";

export const Page404 = () => {
  return (
    <div>
      <h1>Page not found.</h1>
      <Link to="/">Go back top.</Link>
    </div>
  );
};

実際に配置するのはSwitchコンポーネントの中の最下部に配置します。

Switchコンポーネントの最下部にpathを*のワイルドカードで配置することでどのURLにもヒットしなかった時に必ずヒットさせることができます。


import { Switch, Route } from "react-router-dom";
import { Page404 } from "../Page404";

export const Router = () => {
  return (
    <Switch>
     // ~略~
      <Route path="*">
        <Page404 />
      </Route>
    </Switch>
  );
};

written at 2021/6/10.
銀杏くん
2年間メーカーSEとして勤務した後、プログラミング教育事業へ転職。
タイトルの由来は居酒屋でたまたま銀杏串を食べてる時にブログやろうと思い立ったから。
無駄なく"シンプルなブログ"を目指したい。
※ブログはまだまだ改修中です。