ある仕事で開発しているReactアプリでは、モーダルダイアログ上でページが遷移するので、そのページをReact Routerで管理する事にしました。 React Routerを使うと本来Single Page Application(SPA)であるReactアプリのページにURLを関連付けされ。ブラウザーのバックボタン等が使えたりブックマークも出来るようになり、UX向上に繋がります。
ただし、モーダルダイアログ上のページにURLを関連付けるのは一筋縄では行きませんが、ネットを検索してみると解決方法が出てきました。しかし、React Routerのバージョンが古かったので、それをもとに最新バージョンのReact Routerで動くようにしてみました。
画面の動き
下のアニメーションGIFのように、
トップページ(次へボタンをクリック)→ 次ページ(モーダルダイアログボタンをクリック) →
ダイアログのトップページ(次へボタンをクリック) → ダイアログの次ページ(ブラウザーのバックボタンをクリック)→ ダイアログのトップページ(次へボタンをクリック) → ダイアログの次ページ(閉じるボタンをクリック)→
ダイアログが閉じるて、次ページ(戻るボタンをクリック)→ トップページ
と遷移しています。
ページが変わるたびにURLが変わっており、ダイアログの上でもURLが変わるのでブラウザーのバックボタンを使ってページを戻す事ができます。
コード
全コードはGitHubにも上げてありますが、これから主要な部分を説明します。コードは以下のようなディレクター構造になっています。
src/
├── App.tsx
├── components
│ └── modal.tsx
├── hooks
│ └── useModalRoute.ts
├── index.tsx
└── pages
├── modal_next.tsx
├── modal_top.tsx
├── next.tsx
└── top.tsx
ルート定義 (App.tsx)
App.tsxにはReact RouterのURLに対応するページ(コンポーネント)のルーティングを<Route>
で定義します。
ここで重要なとこは、ダイアログ上のページ遷移している時は、ダイアログ上のページだけではなく、ダイアログを表示した下のページも表示しておく必要があるところです。
- ①
useLocation()
はReact Routerの提供するHookで、URLの情報を戻します - ② URL情報のLocation型にはstateというプログラムで使える情報格納エリアがあり、これを使い遷移先に情報を渡せます
- 今回のコードではダイアログを表示したページのLocation情報を保持しています、詳細は次のHookで説明します
- この行でstateがundefinedでなければ、ダイアログが表示されています
- ③
<Routes>
は<Route>
を束ねる命令でlocation=
でルーティングで参照するLocationを指定しています- ②のbackground情報があればダイアログを表示したページ情報が使われます
- background情報が無ければ、通常のURL(
useLocation()
の値)が使われます
- ④ 下のページのURLに対応するコンポーネントのルーティング定義
- ⑤ ダイアログ表示中なら以下のルーティング定義を実行します
- ⑥ React Routerバージョン6で入ったNested Routesを使い
<Modal>
コンポーネントでモーダルダイアログを表示し、その上でダイアログ上のページを表示しています、詳細はダイアログコンポーネントで説明します - ⑦ ダイアログ上のページのURLに対応するコンポーネントのルーティング定義
import { Route, Routes, useLocation } from "react-router-dom";
import { BackgroundLocation } from "./hooks/useModalRoute";
import Modal from "./components/modal";
import Next from "./pages/next";
import Top from "./pages/top";
import ModalNext from "./pages/modal_next";
import ModalTop from "./pages/modal_top";
const App = () => {
const location = useLocation(); // ← ①
const background = (location.state as BackgroundLocation)?.background; // ← ②
return (
<>
<Routes location={background || location}> {/* ← ③ */}
<Route path="/next" element={<Next />} /> {/* ← ④ */}
<Route path="/" element={<Top />} />
</Routes>
{background && ( {/* ← ⑤ */}
<Routes>
<Route path="/" element={<Modal />}> {/* ← ⑥ */}
<Route path="/modal-top" element={<ModalTop />} /> {/* ← ⑦ */}
<Route path="/modal-next" element={<ModalNext />} />
</Route>
</Routes>
)}
</>
);
};
export default App;
Hook (hooks/useModalRoute.ts)
モーダルダイアログの管理とページ遷移を扱うHookを作りました。ここではダイアログを表示したページのLoaction情報を保存するために、ステート管理ライブラリーRecoilを使っています。
- ① Recoilで管理する情報の定義
- 管理するデータの型
BackgroundLocation
は、オブジェクトでLoaction情報またはundefined
です - 初期値は
{background: undefined}
です
- 管理するデータの型
- ②
useNavigate()
はReact Routerの提供するHookで、コードからページ遷移(URL変更)するAPIです - ③
useLocation()
はReact Routerの提供するHookで、URLの情報を戻します - ④
useRecoilState()
はReact標準のuseState()
と同様のAPIです- Recoilのステート管理は、コンポーネントとは無関係にアプリ動作中はずっと存在するグローバルなステートです
- ⑤ ダイアログ表示時に呼び出される関数
- 現在のlocationをRecoilのステート管理に保存します
- 引数で指定されたURLにnavigateで遷移します
- ⑥ ダイアログ終了時に呼び出される関数
- Recoilのステート管理に保存されているlocationに遷移します
パス/クエリー
を組み立てる際に余分な/
を削除しています
- ⑦ ダイアログ表示中のページ遷移時に呼び出される関数
navigate()
のオプション引数でstateを追加しているが肝です
import { Location, useLocation, useNavigate } from "react-router-dom";
import { atom, useRecoilState } from "recoil";
export type BackgroundLocation = { background: Location | undefined };
const backgroundLocationState = atom<BackgroundLocation>({ // ← ①
key: "backgroundLocation",
default: {
background: undefined
}
});
const useModalRoute = () => {
const navigate = useNavigate(); // ← ②
const location = useLocation(); // ← ③
const [backgroundLocation, setBackgroundLocation] =
useRecoilState(backgroundLocationState); // ← ④
const startModalPath = (to: string) => { // ← ⑤
setBackgroundLocation({ background: location });
navigate(to, { state: { background: location } });
};
const endModalPath = () => { // ← ⑥
const background = backgroundLocation.background;
navigate(`${background?.pathname.replace(/\/+$/, "")}/${background?.search}`);
};
const goModalPath = (to: string) => { // ← ⑦
navigate(to, { state: backgroundLocation });
};
return { startModalPath, endModalPath, goModalPath };
};
export default useModalRoute;
トップページ (pages/top.tsx)
最初に表示されるページ。とくに説明する必要はないと思います。
import { Button, Container } from "@mui/material";
import { Link } from "react-router-dom";
const Top = () => {
return (
<Container maxWidth="lg">
<h2>トップページ</h2>
<p>
EY-Officeの教育の特色は、長年実際にサービスやアプリ開発を行ってきた開発者が講師・・・
</p>
<Button component={Link} to="/next" variant="contained">
次へ
</Button>
</Container>
);
};
export default Top;
次ページ (pages/next.tsx)
次のページ。モーダルダイアログの表示はuseModalRoute()
HookのstartModalPath()
関数を使いダイアログ用のURL/modal-top
に遷移しダイアログを表示します。
import { Button, Container, Stack } from "@mui/material";
import { Link } from "react-router-dom";
import useModalRoute from "../hooks/useModalRoute";
const Next = () => {
const { startModalPath } = useModalRoute();
return (
<Container maxWidth="lg">
<h2>次ページ</h2>
<p>
EY-Officeの教育の特色は、長年実際にサービスやアプリ開発を行ってきた開発者が講師・・・
</p>
<Stack direction="row" spacing={2}>
<Button onClick={() => startModalPath("/modal-top")} variant="contained">
モーダルダイアログ
</Button>
<Button component={Link} to="/" variant="outlined">
戻る
</Button>
</Stack>
</Container>
);
};
export default Next;
ダイアログコンポーネント (components/modal.tsx)
モーダルダイアログの枠組みになるコンポーネントです。
- ① ダイアログの表示ON/OFFのステート
- ② ダイアログを閉じる関数
useModalRoute()
HookのendModalPath()
関数を呼びだしています- ダイアログの表示ステートをOFFに設定
- ③ Material UIのダイアログを使いモーダルダイアログを作成
- ④ React Routerバージョン6で入ったNested Routesの機能で子コンポーネントのレンダー結果がここに入ります
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { useState } from "react";
import { Outlet } from "react-router-dom";
import useModalRoute from "../hooks/useModalRoute";
const Modal = () => {
const [open, setOpen] = useState(true); // ← ①
const { endModalPath } = useModalRoute();
const closeModal = () => { // ← ②
endModalPath();
setOpen(false);
};
return (
<Dialog open={open} onClose={closeModal}> // ← ③
<DialogTitle>モーダルダイアログ</DialogTitle>
<DialogContent>
<Outlet /> // ← ④
</DialogContent>
</Dialog>
);
};
export default Modal;
ダイアログ・トップページ (pages/modal_top.tsx)
ダイアログの最初のページ。次へボタンではuseModalRoute()
HookのgoModalPath()
関数を使いダイアログ・次ページのURL/modal-next
に遷移しています。
import { Button, DialogActions, DialogContentText } from "@mui/material";
import useModalRoute from "../hooks/useModalRoute";
const ModalTop = () => {
const { goModalPath } = useModalRoute();
return (
<>
<DialogContentText>
<h2>トップページ</h2>
EY-Officeの教育の特色は、長年実際にサービスやアプリ開発を行ってきた開発者が講師・・・
</DialogContentText>
<DialogActions>
<Button onClick={() => goModalPath("/modal-next")} variant="contained">
次へ
</Button>
</DialogActions>
</>
);
};
export default ModalTop;
ダイアログ・次ページ (pages/modal_next.tsx)
ダイアログの次ページ。閉じるボタンではuseModalRoute()
HookのendModalPath()
関数を呼びだしダイアログ表示前のページに遷移しています。
import { Button, DialogActions, DialogContentText } from "@mui/material";
import useModalRoute from "../hooks/useModalRoute";
const ModalNext = () => {
const { endModalPath } = useModalRoute();
return (
<>
<DialogContentText>
<h2>次ページ</h2>
EY-Officeの教育の特色は、長年実際にサービスやアプリ開発を行ってきた開発者が講師・・・
</DialogContentText>
<DialogActions>
<Button onClick={() => endModalPath()} variant="outlined">
閉じる
</Button>
</DialogActions>
</>
);
};
export default ModalNext;
まとめ
モーダルダイアログの上でページ遷移するのがUX的に良いのかはともかく、このようにすると実装できます。
またReact Routerバージョン6で入ったNested Routesはレイアウトコンポーネントのような使い方が出来てて便利ですね。