Next.jsでダイアログの状態をパラメータで保持する

Next.js, React
2022-01-13

Next.js (React)でモーダルやページ内でステップを表示する際に別の内容を表示しようとする際に、リロードやブラウザバックにより、その状態を保持したり、ユーザが思ったような挙動をしたい場合、location.replaceメソッドを使って、ページ遷移をせず、その状態をブラウザに保持することができます。

例えば以下のようにモーダルが開いているようなUIの場合、通常はjs内の変数(state)でモーダルの状態を管理することになるため、リロードした場合はモーダルが閉じてしまいます。

An image from Notion

なので、この場合に例えば localhost:3000?id=2 のようにURLパラメータをつけることでリロードした際にURLに id=* がある場合、モーダルを表示するというような設計をしたいと思います。

また、stepperUIのように、同一ページ内でナビゲートするようなUIであってもパラメータで現在のページ位置を保持するようになってれば、ユーザがブラウザの戻るを押してもページ自体はそのままで、ナビゲーションだけを戻すことが可能になります。

An image from Notion
https://material.io/archive/guidelines/components/steppers.htmlahttps://material.io/archive/guidelines/components/steppers.html

Next.jsでの実装例

replaceの例

replaceを使う場合は以下のような要件になります。

  • モーダル表示時など、なにかのON・OFFの状態をURLに保持
  • リロード時はそのURLをチェックし、UIを復元
  • ブラウザの戻る場合は前のページに遷移(通常のブラウザ動作と同じ)

Next.jsでは useRouter (および withRouter )でhistoryやURLのパラメータの取得・操作ができます。Reactのみ場合でもreact-routerなどを使えば同様なことができると思います。

ページの実装例は以下になります。(※ UIのライブラリとして chakra-ui を使っています)

import { NextPage } from 'next';
import { useRouter } from 'next/router';
import * as React from 'react';

import {
  Box, Container, HStack, Modal, ModalContent, ModalOverlay, SimpleGrid
} from '@chakra-ui/react';

interface ParamsProps {}

const PARAMS_KEY = "id";

/**
 * with params
 */
const Index: NextPage<ParamsProps> = () => {
  const { query, replace } = useRouter();
  const [selectedId, setSelectedId] = React.useState("");

  // パラメータにid=xがある場合、モーダルをデフォルトで表示する
  React.useEffect(() => {
    if (query && query[PARAMS_KEY]) {
      const id = query[PARAMS_KEY] as string;
      setSelectedId(id);
    }
  }, [query]);

  // 選択時、urlを更新する
  React.useEffect(() => {
    if (selectedId) {
      replace(`${location.pathname}?${PARAMS_KEY}=${selectedId}`);
    } else {
      replace(location.pathname);
    }
  }, [selectedId]);

  const handleCloseOverlay = React.useCallback(() => {
    setSelectedId("");
  }, []);

  const handleSelectId = React.useCallback((id) => {
    setSelectedId(id);
  }, []);

  return (
    <Container>
      <Modal
        blockScrollOnMount={false}
        isOpen={!!selectedId}
        onClose={handleCloseOverlay}
      >
        <ModalOverlay />
        <ModalContent p={16}>Selected id is {selectedId}</ModalContent>
      </Modal>
      <HStack column={3} spacing={4} px={4}>
        {[1, 2, 3].map((id) => (
          <Box
            onClick={() => {
              handleSelectId(id.toString());
            }}
            key={id}
          >
            <span>ID:{id}</span>
          </Box>
        ))}
      </HStack>
    </Container>
  );
};

export default Index;

解説など

React.useEffect(() => {
  if (query && query[PARAMS_KEY]) {
    const id = query[PARAMS_KEY] as string;
    setSelectedId(id);
  }
}, [query]);

このuseEffectの部分で今のURL上にパラメータがあるかどうかを判定します。URLパラメータがある場合はその値をstateに設定します。(実際にはパラメータは配列になることがあったりするので、もう少し厳密なvalidationが必要になります)

React.useEffect(() => {
  if (selectedId) {
    replace(`${location.pathname}?${PARAMS_KEY}=${selectedId}`);
  } else {
    replace(location.pathname);
  }
}, [selectedId]);

stateが更新された場合このuseEffectが動作して、replaceメソッドを使いhistoryを更新します。

selectIdがunsetされた場合は現在のパス(例えば "/" とか)に変更します。

モーダルを表示するかどうか(今回はchakra-uiのModalを利用しています)は、stateにidがあるかどうかで判定します。

<Modal
  blockScrollOnMount={false}
  isOpen={!!selectedId}
  onClose={handleCloseOverlay}
>

pushの場合

pushを使う場合は以下のような要件になります。

  • stepperUIなど、連続したナビゲーションがある場合のそのどこかの状態を状態をURLに保持
  • リロード時はそのURLをチェックし、UIを復元
  • ブラウザの戻る場合はhistoryが更新されるので前のナビゲーションがあればその状態に遷移(初期状態以外はページ自体は遷移しない)

(詳細は端折りますが、)replaceとの差分としては以下のようになります。

React.useEffect(() => {
  if (query) {
    if (query[PARAMS_KEY]) {
      const id = query[PARAMS_KEY] as string;
      setSelectedId(id);
    } else {
      setSelectedId("");
    }
  }
}, [query]);

// ナビゲートを実行する場合
const handleGoNext = React.useCallback((id) => {
  push(`${location.pathname}?${PARAMS_KEY}=${id}`);
}, []);

// 初期状態にする場合など
const handleCloseOverlay = React.useCallback(() => {
  push(location.pathname);
}, []);

表示する部分ではselectedIdの値が何になっているかで、表示するものを変更します。

Written by Kyohei Tsukuda who lives and works in Tokyo 🇯🇵 , building useful things 🔧.