EY-Office ブログ

React routerを使ったモーダルダイアログ上のページ遷移

ある仕事で開発しているReactアプリでは、モーダルダイアログ上でページが遷移するので、そのページをReact Routerで管理する事にしました。 React Routerを使うと本来Single Page Application(SPA)であるReactアプリのページにURLを関連付けされ。ブラウザーのバックボタン等が使えたりブックマークも出来るようになり、UX向上に繋がります。

ただし、モーダルダイアログ上のページにURLを関連付けるのは一筋縄では行きませんが、ネットを検索してみると解決方法が出てきました。しかし、React Routerのバージョンが古かったので、それをもとに最新バージョンの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はレイアウトコンポーネントのような使い方が出来てて便利ですね。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ