1
0

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.

React Router + Redux + MUI ではまった件

Posted at

はじめに

ReactでSPAを実装する時に、Reduxも使ってstateを管理することが多いと思います。私も実際その組み合わせにMUIを加えてアプリケーションを実装していたのですが、思わぬところで問題が発生して余計な時間を取られてしまったので、その詳細を記載しておこうと思います。

アプリケーションの作成

今回はViteを使って雛形のReact Applicationを作成します。

$ npm create vite@latest react-router-redux-mui -- --template react

Scaffolding project in /root/react-router-redux-mui...

Done. Now run:

  cd react-router-redux-mui
  npm install
  npm run dev

$ cd react-router-redux-mui/
$ npm i

added 86 packages, and audited 87 packages in 17s

9 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

アプリケーションが作成されたら、以下のコマンドで起動できます。

$ npm run dev

> react-router-redux-mui@0.0.0 dev
> vite


  vite v2.9.14 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

  ready in 1142ms.

ログにあるようにlocalhostの3000番にアクセスすればアプリケーションの動作を確認できます。
スクリーンショット 2022-07-10 14.41.15.png
起動できることが確認できたら、余分なソースを削除し、ESLintの構成もしておきます。

$ npx eslint --init

MUI

まずはMUIを導入します。こちらの手順に沿って導入します。

$ npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

index.htmlも更新します。

index.html
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>

これでMUIを使う準備ができました。App.jsを書き換えてみます。

App.js
import { Box, Button } from "@mui/material";
import React from "react";
import "./App.css";

function App() {
  return (
    <Box className={"App"}>
      Hello World!
      <Button variant="contained">Click</Button>
    </Box>
  );
}

export default App;

このようにMUIのボタンが表示されることが確認できました。
スクリーンショット 2022-07-10 17.30.30.png

React Router

次にReact Routerを使うための構成を行います。Quick Startに従って構成していきます。

$ npm i react-router-dom@6

新たにMenu.jsxというページを作っておきます。

Menu.jsx
import React from "react";
import { Box } from "@mui/system";
import { Link } from "@mui/material";

export default function Menu() {
    return (
        <Box>Menu Page <Link href="/">Home</Link></Box>
    );
}

存在しないパスを指定した時のページも作っておきます。

NoRoute.jsx
import React from "react";
import { Link, Stack, Box } from "@mui/material";

export default function NoRoute() {
    return (
        <Stack direction="row" spacing={3}>
            <Box>Not Found</Box>
            <Link href="/">Back to Home</Link>
        </Stack>
    );
}

App.jsxMenu.jsxへ飛べるように変更しておきます。

App.jsx
import { Box, Button, Link, Stack } from "@mui/material";
import React from "react";
import "./App.css";

function App() {
  return (
    <Stack direction="row" spacing={3} className={"App"}>
      <Box>Hello World!</Box>
      <Button variant="contained">Click</Button>
      <Link href="/menu">Menu</Link>
    </Stack>
  );
}

export default App;

main.jsxにReact Routerを使ったルーティングの設定を記述します。

main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./index.css";
import App from "./App";
import NoRoute from "./NoRoute";
import Menu from "./Menu";

ReactDOM.createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<App/>} />
      <Route path="/menu" element={<Menu/>} />
      <Route path="*" element={<NoRoute/>} />
    </Routes>
  </BrowserRouter>
);

トップページのリンクをクリックすると、/menuに遷移してページが表示されるのがわかると思います。

スクリーンショット 2022-07-10 17.49.54.png

Redux/Redux Toolkit

Reduxを導入します。素で構成してもいいのですが、公式でもRedux Toolkitを使って構成するのが推奨なようなので、Redux ToolkitのQuick Startに沿って構成してみます。

$ npm install @reduxjs/toolkit react-redux

added 5 packages, and audited 275 packages in 2s

70 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

必要なモジュールをインストールしたら、まずはstoreを用意します。

store.js
import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
    reducer:{}
});

main.jsxで上記のstoreを組み込みます。

main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./index.css";
import App from "./App";
import NoRoute from "./NoRoute";
import Menu from "./Menu";
import { Provider } from "react-redux";
import { store } from "./store";

ReactDOM.createRoot(document.getElementById("root")).render(
  <Provider store={store}>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App/>} />
        <Route path="/menu" element={<Menu/>} />
        <Route path="*" element={<NoRoute/>} />
      </Routes>
    </BrowserRouter>
  </Provider>
);

次にactionやreducerを作るため、sliceを作っていきます。ここではQuick StartにあるcounterSliceをほぼそのまま使っています。

counterSlice.js
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;

これを使ってApp.jsにカウンターを実装してみます。

App.js
import { Box, Button, Link, Stack } from "@mui/material";
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import "./App.css";
import { decrement, increment } from "./counterSlice";

function App() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  
  return (
    <Stack direction="row" spacing={3} className={"App"}>
      <Box>Count: {count}</Box>
      <Button variant="contained" onClick={() => dispatch(increment())}>+1</Button>
      <Button variant="contained" onClick={() => dispatch(decrement())}>-1</Button>
      <Link href="/menu">Menu</Link>
    </Stack>
  );
}

export default App;

このようにカウンターが動作することが確認できました。
スクリーンショット 2022-07-10 18.18.33.png

別のページで使う

しかし同じページでカウントが増減できてもそれは普通にReactのstate等でも可能なので、Reduxを使ってる価値はほぼありません。ですので別のページから同じカウントを参照してみます。

遷移先であるMenu.jsxページで先ほどと同じcountを表示させてみます。

Menu.jsx
import React from "react";
import { Box } from "@mui/system";
import { Link } from "@mui/material";
import { useSelector } from "react-redux";

export default function Menu() {
    const count = useSelector((state) => state.counter.value);
    return (
        <Box>Count: {count} <Link href="/">Home</Link></Box>
    );
}

するとどうでしょう。
スクリーンショット 2022-07-10 18.24.28.png
Appの方でカウントを変更してもMenuの方に遷移するとリセットされて0になってしまいました。

原因

この原因に気づくまでほぼ丸一日を要しました(!)が、わかってみると単純でした。原因はこれでした。

      <Link href="/menu">Menu</Link>

正解は

      <Link to="/menu">Menu</Link>

です。これだとただのattributeの選び間違いのようにも見えますが、実際は<Link>の実体が異なります。

動かない方は

import { Link } from "@mui/material";

正解は

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

MUIのLinkは

The Link component allows you to easily customize anchor elements with your theme colors and typography styles.

ですので基本的に<a>タグそのものになり、hrefに指定されたURLに遷移することになります。なので基本的にはそのURLを直叩きした時と同様、ページ全体がロードされるようです。それに対してreact-router-domのLinkはSPAの中でのページ遷移であり、ブラウザのURLは変わっているもののページ全体がロードされることはありません。なのでreduxのstateもそのまま保持されているようです。

この変更を施して実際にMenuページへ移動してみると無事カウントが表示されました!
スクリーンショット 2022-07-10 18.45.03.png
このページにはカウントを変更するボタンがないので、他のページ(コンポーネント)で変更された値を正しく参照できていることがわかると思います。

MUIのcomponentを使ってreact-router-domのroutingを使うには、以下のようにすれば可能です。

<Button variant="contained" component={Link} to="/">Home</Button>

こうするとMUIのデザインのボタンでreact-router-domのroutingを行うことができます。
スクリーンショット 2022-07-10 18.54.20.png

まとめ

React Route + Redux + MUI の組み合わせで生じた問題について記述しました。原因がわかってみれば単純だったのですが、なにぶん同じ名前のコンポーネントだったので気づくのに時間がかかってしまいました。同じ問題に直面してる方の一助になれば幸いです。

Caveat

本来はreact-routerとreduxを一緒に使うには、react-router-reduxやその後継のようなconnected-react-routerなどの導入が必要なようなのですが、少なくとも上記の使い方であれば導入しなくても大丈夫なようです。ですが問題が起きる可能性はあるかもしれません。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?