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

# はじめに

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


# アプリケーションの作成

今回は[Vite](https://vitejs.dev)を使って雛形の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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/1852d2b0-7bc2-c175-88db-2cc825c04504.png)
起動できることが確認できたら、余分なソースを削除し、ESLintの構成もしておきます。

```shell
$ npx eslint --init
```

# MUI

まずはMUIを導入します。[こちら](https://mui.com/material-ui/getting-started/overview/)の手順に沿って導入します。

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

`index.html`も更新します。

```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`を書き換えてみます。

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/7aa11244-6d0d-98ee-c51c-a095efa775d3.png)


# React Router

次にReact Routerを使うための構成を行います。[Quick Start](https://reactrouter.com/docs/en/v6/getting-started/overview)に従って構成していきます。

```
$ npm i react-router-dom@6
```

新たに`Menu.jsx`というページを作っておきます。
```javascript: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>
    );
}
```

存在しないパスを指定した時のページも作っておきます。
```javascript: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.jsx`も`Menu.jsx`へ飛べるように変更しておきます。
```javascript: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を使ったルーティングの設定を記述します。

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/1943ed7e-3797-ec08-0e53-36967e9d0350.png)


# Redux/Redux Toolkit

Reduxを導入します。素で構成してもいいのですが、公式でもRedux Toolkitを使って構成するのが推奨なようなので、[Redux ToolkitのQuick Start](https://redux-toolkit.js.org/tutorials/quick-start)に沿って構成してみます。

```shell
$ 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を用意します。

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

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

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

```javascript: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`をほぼそのまま使っています。

```javascript: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`にカウンターを実装してみます。
```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/7fa05232-b5a8-b64e-9ab8-6aac883548b5.png)


# 別のページで使う

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

遷移先である`Menu.jsx`ページで先ほどと同じ`count`を表示させてみます。
```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/fb9401a9-41da-80c1-9ede-0021ef28fa25.png)
`App`の方でカウントを変更しても`Menu`の方に遷移するとリセットされて0になってしまいました。

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

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

正解は

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

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

動かない方は
```javascript
import { Link } from "@mui/material";
```

正解は
```javascript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/547c3717-1bec-8313-26f6-25670ca6bcd8.png)
このページにはカウントを変更するボタンがないので、他のページ（コンポーネント）で変更された値を正しく参照できていることがわかると思います。

MUIのcomponentを使ってreact-router-domのroutingを使うには、以下のようにすれば可能です。
```javascript
<Button variant="contained" component={Link} to="/">Home</Button>
```
こうするとMUIのデザインのボタンでreact-router-domのroutingを行うことができます。
![スクリーンショット 2022-07-10 18.54.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632713/5a4bcdf0-fc7a-c593-918b-a3ce6b1c6457.png)


# まとめ

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


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