Development

【React-Redux】react-routerでルーティングを実装編 [初心者入門6日目]

React × Reduxの連載記事の6回目です。

今回はページングの処理を実装していきたいと思います。

前回の状態からサンプルページをいくつか追加し、routerによるページングができるところまでが今回のゴールです。

React Redux環境でページング処理を行うために、今回はreact-router-domのモジュールを使って実装します。

この記事でわかること
  • react-router-domを使ったページルーティングの実装
  • connected-react-routerを使ったreduxでのreact router統合の実装

この記事のターゲットとなる環境

現在の環境状況や前提条件を書いておきます。

前提条件
  • Mac OS Catalina ver 10.15.3
  • 5日目の記事内容まで理解している
  • 環境ターゲット(2020-03-16現在最新)
    • React16.13
    • Redux7.2
    • webpack4.42
    • babel7.8.7
    • eslint6.8
    • Material-UI4.9.5
    • react-router-dom5.1.2

これまでのReact × Reduxの連載記事はこちらからどうぞ

【2020年版】React × Redux 初心者入門 React × Redux の初心者向け連載記事です。 ネットで調べると古いバージョンの記事が溢れているので、できるだけ最新環境...

必要なモジュールをインストールする

reactでルーティングの処理やページングのhistory操作を行うためにreact-routerとreact-router-domというライブラリをインストールします。

$ npm install --save react-router react-router-dom

React Router

現時点(2020/03/16)でのバージョンは5.1.2

公式サイトに詳しいドキュメントがあるので一読することをオススメします。

完成予定と今回のポイント

完成予定構成

–Login
–Top
–Counter
–Sample
└–SampleChild1
└–SampleChild2

こんな感じでログインページ、Topメニューページ、カウンターページ、サンプルページを作ってみたいと思います。

サンプルページには、子ページを2つ用意して、URLパラメータで切り替わるようにしたいと思います。

また、今回はページングの実装のみなので、中身については実装しません。

実装のポイント

  • React Redux環境ですが、今回はReduxとの統合は行いません。
    統合を行う場合はConnected React Routerを使ったりすればできると思います。
  • 子コンポーネントでもhistoryを使いますが、propsを経由せずhooksを使って触ろうと思います。

ログインページの作成

ページングで切り替わる対象のページごとに以下のcontainerとcomponentを作成しておきます。

LoginContainer.jsxの作成

http://localhost:8080/loginで表示される用のページのコンテナです。

後述するLoginComponent.jsxを呼ぶだけです。

useEffectを使ってLogin画面でやるべき初期処理を書けるようにしてあります。

LoginContainer.jsxの全体像

import React, { useEffect } from 'react';

import Login from '../../components/login/LoginComponent';

const LoginContainer = () => {
  useEffect(() => {
    console.log('LoginContainer:useEffectによる初回処理');
  }, []);
  return (
    <div>
      <Login />
    </div>
  );
};

export default LoginContainer;

LoginComponent.jsxの作成

LoginComponentではTopページへ遷移させるログインボタンを置いておきます。

ページ遷移はuseHistoryで参照できるhistoryを操作して行います。

LoginComponent.jsxの全体像

import React from 'react';
import { useHistory } from 'react-router';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import ExitToApp from '@material-ui/icons/ExitToApp';
import * as Colors from '@material-ui/core/colors';


const useStyles = makeStyles((theme) => ({
  button: {
    padding: theme.spacing(1),
    color: Colors.common.white,
  },
}));

const LoginComponent = () => {
  const classes = useStyles();
  const history = useHistory();

  const handleOnClickLogin = () => {
    alert('login clicked');
    history.push('/');
  };

  return (
    <div style={{ width: '100%' }}>
      <div className="centerTable">
        <h2>
          ログインページ
        </h2>
        <Button
          className={classes.button}
          variant="contained"
          color="primary"
          fullWidth
          startIcon={<ExitToApp />}
          onClick={handleOnClickLogin}
        >
          LOGIN
        </Button>
      </div>
    </div>
  );
};

export default LoginComponent;

今まではhistoryをpropsで受け取ったり、withRouterを使って引き継いだりしてましたが、hooksのuseHistoryが使えるようになったことによりかなり簡潔に書けるようになりました!

Counterページの修正

前回までで作成していたcounterComponentを呼び出すcontainerを用意します。

今まではAppContainerから呼び出されていたcounterComponentをCounterContainerから呼び出すようにします。

CounterContainer.jsxの作成

AppContainerで作成していた関数memo(useActions)を新しく作成したCounterContainer.jsxで作成するようにして、componentにpropsとして渡しています。

import React from 'react';
import { useSelector } from 'react-redux';

import Counter from '../../components/counter/CounterComponent';
import { increment, decrement } from '../../slices/counter';
import useActions from '../../slices/userActions';

const CounterContainer = () => {
  const counterActions = useActions({ increment, decrement });
  const counter = useSelector((state) => state.counter);

  return (
    <div>
      <Counter counter={counter} counterActions={counterActions} />
    </div>
  );
};

export default CounterContainer;

CounterComponent.jsxはそのまま

counterのコンポーネントについては今までのまま特に変更ありません。

import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import AddIcon from '@material-ui/icons/AddCircle';
import RemoveIcon from '@material-ui/icons/RemoveCircle';

const useStyles = makeStyles((theme) => ({
  button: {
    margin: theme.spacing(1),
  },
}));

const CounterComponent = (props) => {
  const { counter, counterActions } = props;
  const classes = useStyles();

  return (
    <div style={{ width: '100%' }}>
      <div className="centerTable">
        <h2>
          count={counter.value}
        </h2>
        <Button
          className={classes.button}
          variant="contained"
          color="primary"
          startIcon={<AddIcon />}
          onClick={() => counterActions.increment()}
        >
          増加
        </Button>
        <Button
          className={classes.button}
          variant="contained"
          color="secondary"
          startIcon={<RemoveIcon />}
          onClick={() => counterActions.decrement()}
        >
          減少
        </Button>
      </div>
    </div>
  );
};

CounterComponent.propTypes = {
  counter: PropTypes.shape({
    value: PropTypes.number,
  }).isRequired,
  counterActions: PropTypes.shape({
    increment: PropTypes.func,
    decrement: PropTypes.func,
  }).isRequired,
};

export default CounterComponent;

Topページの作成

こちらもLoginページと同様にcontainerとcomponentを用意します。

TopContainer.jsxの作成

import React, { useEffect } from 'react';

import Top from '../../components/top/TopComponent';

const TopContainer = () => {
  useEffect(() => {
    console.log('TopContainer:useEffectによる初回処理');
  }, []);
  return (
    <div>
      <Top />
    </div>
  );
};

export default TopContainer;

TopComponent.jsxの作成

TopComponentにはCounterページへの遷移ボタンと、サンプルページ1、2への遷移ボタンを用意します。

import React from 'react';
import { useHistory } from 'react-router';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const useStyles = makeStyles((theme) => ({
  button: {
    margin: theme.spacing(1),
  },
}));

const TopComponent = () => {
  const classes = useStyles();
  const history = useHistory();

  return (
    <div style={{ width: '100%' }}>
      <div className="centerTable">
        <h2>
          トップページ
        </h2>
        <div style={{ textAlign: 'center' }}>
          <Button
            className={classes.button}
            variant="contained"
            color="secondary"
            onClick={() => history.push('/counter')}
          >
            カウンターページ
          </Button>
        </div>
        <div>
          <Button
            className={classes.button}
            variant="contained"
            onClick={() => history.push('/sample?page=1')}
          >
            サンプルページ1
          </Button>
          <Button
            className={classes.button}
            variant="contained"
            onClick={() => history.push('/sample?page=2')}
          >
            サンプルページ2
          </Button>
        </div>

      </div>
    </div>
  );
};

export default TopComponent;

サンプルページの作成

サンプルページは以下の3つのファイルを作成します。

  • containers/sample/SampleContainer.jsx
  • components/sample/Sample1Component.jsx
  • components/sample/Sample2Component.jsx

SampleContainer.jsxの作成

SampleContainer.jsxでは、ページのクエリストリングを取得して、サンプル1かサンプル2のどちらかのページのコンポーネントを表示するようにします。

クエリストリングはHooksのuseLocationでURLのlocationを取得し、そこからURLSearchParamsを使ってQueryStringを取得していきます。

import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

import Sample1 from '../../components/sample/Sample1Component';
import Sample2 from '../../components/sample/Sample2Component';
import NotFound from '../NotFound';

const useQuery = () => {
  // useLocationでlocationを取得
  const location = useLocation();

  // locationからクエリストリングを取得
  return new URLSearchParams(location.search);
};

const SampleContainer = () => {
  useEffect(() => {
    console.log('SampleContainer:useEffectによる初回処理');
  }, []);

  // 対象のページをクエリストリングから取得
  const query = useQuery();
  const targetPage = query.get('page');

  if (targetPage === '1') {
    return (
      <div>
        <Sample1 />
      </div>
    );
  }
  if (targetPage === '2') {
    return (
      <div>
        <Sample2 />
      </div>
    );
  }
  return (
    <div>
      <NotFound />
    </div>
  );
};

export default SampleContainer;

SampleComponet.jsxの作成

サンプルページ1、サンプルページ2用のコンポーネントを2つ作成します。

ページ名とTopページに戻るボタンだけがあるページにしました。

import React from 'react';
import { useHistory } from 'react-router';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import ExitToApp from '@material-ui/icons/ExitToApp';
import * as Colors from '@material-ui/core/colors';


const useStyles = makeStyles((theme) => ({
  button: {
    padding: theme.spacing(1),
    color: Colors.common.white,
  },
}));

const Sample1Component = () => {
  const classes = useStyles();
  const history = useHistory();

  const handleOnClickToTop = () => {
    history.push('/');
  };

  return (
    <div style={{ width: '100%' }}>
      <div className="centerTable">
        <h2>
          サンプルページ1
        </h2>
        <Button
          className={classes.button}
          variant="contained"
          color="primary"
          fullWidth
          startIcon={<ExitToApp />}
          onClick={handleOnClickToTop}
        >
          TOPへ戻る
        </Button>
      </div>
    </div>
  );
};

export default Sample1Component;

Sample2Component.jsxは上記のハイライトしてある行をSample2に変更したファイルを用意してください。

以上で必要なページの準備は完了しました。

続いてルーティングを制御するAppContainerを修正していきます。

ルーティング制御の実装

ルーティングの制御は全てのコンテナをAppContainerに実装したいと思います。

route定義を行うためにBrowserRouterとSwitch,Routeをインポートして使います。

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'

AppContainer.jsxの修正

ルーティングを行う箇所をBrowserRouter(Router)でくくり、Switchタグ内にpathに対応するcontainerをRouteタグで定義していきます。

直感的にルーティングが行えるのでみてもらえればわかると思います。

counterについても呼び出していたコンポーネントは削除して、コンテナに切り替えましょう。

import React, { useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import * as Colors from '@material-ui/core/colors';

import Header from '../components/header/HeaderComponent';
import TopContainer from './top/TopContainer';
import LoginContainer from './login/LoginContainer';
import CounterContainer from './counter/CounterContainer';
import SampleContainer from './sample/SampleContainer';
import NotFound from './NotFound';

const theme = createMuiTheme({
  palette: {
    primary: Colors.lightBlue,
    secondary: Colors.yellow,
  },
});

const AppContainer = () => {
  useEffect(() => {
    console.log('AppContainer:useEffectによる初回処理');
  }, []);
  return (
    <MuiThemeProvider theme={theme}>
      <div>
        <Header />
        <div id="contents">
          <Router>
            <Switch>
              <Route path="/" exact>
                <TopContainer />
              </Route>
              <Route
                path="/login"
                exact
                component={LoginContainer}
              />
              <Route
                path="/counter"
                exact
                render={() => <CounterContainer />}
              />
              <Route
                path="/sample"
                exact
                render={() => <SampleContainer />}
              />
              <Route component={NotFound} />
            </Switch>
          </Router>
        </div>
      </div>
    </MuiThemeProvider>
  );
};

export default AppContainer;

Routeタグの書き方は色々できるのですが、今回は

  • 子要素に追加する方法
  • componentに指定する方法
  • renderで指定する方法

の3種類を書いておいきました。

詳しくは公式サイトを一読ください。

componentに指定する方法では子コンポーネントにpropsを渡す事はできません。

propsを渡す必要がある場合は、子要素かrenderを使うようにすればいいと思います。

対応するpathがない時用のページを追加

AppContainerのルート定義の最後にpathを指定していないcomponentを追加しました。

これは対応するpathが存在しないときに表示されるので、NotFoundというcontainerを作って設定してあります。

import React from 'react';

import notFoundImage from '../src/assets/image/not_found.png';

const NotFound = () => (
  <div className="row" style={{ textAlign: 'center', height: '100%' }}>
    <br />
    <br />
    <br />
    <img
      src={notFoundImage}
      style={{
        minWidth: '100px',
        maxWidth: '300px',
        width: '100%',
        height: '100%',
      }}
      alt="NotFound"
    />
    <h1>404 Not Found</h1>
    <br />
    <h3>このURLは存在しません。</h3>
  </div>
);

export default NotFound;

pngファイルを読み込むためにwebpack.configを修正

NotFoundページで表示する画像ファイルをsrc/assets/imageにおきました。

これをjsに読み込むためにwebpack.configに下記のloader設定を追記します。

module: {
    rules: [
      {
        test: /\.(png|gif|jpg)$/,
        loader: 'file-loader?context=src/assets&name=[path][name].[ext]',
      },
    ],
  },

以上でルーティング制御の実装は全て完了です。

実行してみる

npm startでdev-serverを起動した後に、http://localhost:8080/loginをブラウザに入力して実行してみましょう。

ログインページが表示されればOKです。

ログインページ

package.jsonののstartスクリプトに–history-api-fallbackがついていない場合は、/loginなどの直パスのアクセスはできないので注意してください。

ログインボタンをクリックするとTopページに遷移するはずです。

トップページ

トップページからはカウンターページ、サンプルページ1、サンプルページ2へ遷移可能です。

サンプルページも正しくクエリストリングによってルーティングされていると思います。

カウンターページ
サンプルページ1
サンプルページ2

無事ルーティングが実装できました!!

React Routerでルーティングを実装編まとめ

今回はページを追加してReact Routerを使ったページルーティング制御を実装してみました。

React Routerもバージョンによって実装に仕方がかなり変更されていくモジュールなので、ちゃんと公式サイトのドキュメントを読み解くことをオススメします。

React Routerもhooksに対応したことにより、かなり簡潔に書けるようになったと思いますので、習得コストや導入コストはだいぶ低くなったと思います。

間違っているとこや、わからないところなどはコメント頂ければ嬉しいです。

誰かの参考になれば幸いです。

次回予告

次回予告というかやろうと思ってること

  • Redux Toolkitから作成するDucksパターンで実装する完了
  • ESLintでAirbnbのスタイルガイドを実装する完了
  • Materialデザインを実装する完了
  • カスタムCSSを実装する完了
  • ページルーティングを実装する完了
  • 開発環境の効率化をするためにHotReloadとソースマップを実装する
  • REST通信を実装する
  • ログイン制御のフローを実装する
  • デプロイ方法を検討、実装する

次の記事はこちら

【React-Redux】react-hot-loaderでHMR実装編 [初心者入門7日目] React×Reduxの連載記事の7回目です。 今回は開発の効率をよくするためにHotReload(通称HMR)の実装とソースマ...

勉強するならプログラミングスクールがベスト

今からプログラミングを勉強してプロのエンジニアを目指すなら、プログラミングスクールで勉強するのがベストです!

未経験からプロのエンジニアを育てるオンラインブートキャンプ

最短4週間で未経験からプロを育てるオンラインスクールなので、自宅からプログラミングやアプリ開発を学ぶことができます。


オススメ本

[itemlink post_id=”534″ klabel=Kindleで探す alabel=Amazonで探す rlabel=楽天市場で探す ylabel=Yahoo!ショッピングで探す]

[itemlink post_id=”637″ klabel=Kindleで探す alabel=Amazonで探す rlabel=楽天市場で探す ylabel=Yahoo!ショッピングで探す]

POSTED COMMENT

  1. A より:

    今回も大変楽しく取り組ませていただきました。貴重なコンテンツをありがとうございます。

    一点、この内容には Counter 関連の実装の追加・修正が足りないように思うのですが、「中身については実装しません」とあるように、意図したものなのでしょうか。
    (私はこれが動くように Counter 関連の実装の追加・修正を実施することで動かすことができ、かえって力試しになりましたので問題はありませんでした)

    もし意図していないもの・なんらかのミスであればいけないと思い、コメントさせていただきました。いつもありがとうございます。

    • hiro より:

      Aさん、コメントありがとうございます。
      そして今回も貴重なご指摘ありがとうございます。

      完全にCounterの記載が全く抜けてましたね^^;
      Counterについては前回まで作成してたcomponentをContanerとComponentに展開するイメージで実装していただけたかと思います。

      記事の方も修正しておきますね!

      参考にしていただけてるようでとても嬉しいです。
      引き続きどうぞよろしくお願いします。

COMMENT

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です