💾

ダイアログが閉じるまでコンテンツをキャッシュしてクローズアニメーションさせる方法

に公開

始めに

ダイアログの表示非表示のフラグでシンプルなのはisOpenなどbooleanで管理することですが、対象データの詳細を表示するときなど表示対象を指定する場合は以下のコードのように対象をステートで持ち、nullじゃないときにダイアログを表示するようにすると変数が一つにまとまってスッキリします。

type User = {
  id: number;
  name: string;
  description: string;
};

const App: FC = () => {
  const [selectedUser, setSelectedUser] = useState<User | null>(null);

  return (
    <Box sx={{ p: 1 }}>
      <Button
        variant="outlined"
        onClick={() => {
          setSelectedUser({
            id: 1,
            name: 'ユーザ1',
            description: 'ユーザ1の説明'
          });
        }}
      >
        ユーザ1の詳細を確認する
      </Button>
      <Button
        variant="outlined"
        onClick={() => {
          setSelectedUser({
            id: 2,
            name: 'ユーザ2',
            description: 'ユーザ2の説明'
          });
        }}
      >
        ユーザ2の詳細を確認する
      </Button>

      <Dialog
        // selectedUserに指定があるかどうかでダイアログを開く
        open={selectedUser != null}
        onClose={() => {
          setSelectedUser(null);
        }}
      >
        {/* selectedUserがある時のみUserDetailコンポーネントをrenderする */}
        {selectedUser && (
          <UserDetail
            user={selectedUser}
            onClose={() => {
              setSelectedUser(null);
            }}
          />
        )}
      </Dialog>
    </Box>
  );
}

この考えはReactの公式にも載っており、単純にisOpenselectedUserを両方持ってしまった場合、詳細を表示できないけどダイアログは開いている状態があり得てしまうので、selectedUserのみでまとめてしまうことは良いことだと思っています。

https://ja.react.dev/learn/choosing-the-state-structure#avoid-contradictions-in-state

しかしダイアログの中身がselectedUserがある時しかrenderできないため、ダイアログを閉じる際のクローズアニメーション中に突然消えてしまい、折角のフェードアニメーションができなくなってしまいます。

完全にクローズするまで中身が描画されている時は以下のgifアニメのような表示になります。

このクローズアニメーションを表示するためだけに2変数を持ちたくなくて色々模索して、結果最初のコードのままで2枚目のgifアニメのように綺麗にアニメーションすることができたので記事にまとめたいと思います。

ダイアログが閉じるまでコンテンツをキャッシュする

結論から言うとchildrenをrefでキャッシュしておき、そのキャッシュ条件をダイアログが開いている時だけにするとクローズ直前の要素を維持することができます。

クローズ直前の要素を維持するダイアログ
import { Dialog, type DialogProps } from '@mui/material';
import { useRef, type FC, type ReactNode } from 'react';

export type CacheableDialogProps = DialogProps;

export const CacheableDialog: FC<CacheableDialogProps> = ({
  open,
  children,
  ...restProps
}) => {
  const cachedChildrenRef = useRef<ReactNode>(null);
  // 開いている時だけ表示する子要素を更新する
  if (open) {
    cachedChildrenRef.current = children;
  }

  return (
    <Dialog open={open} {...restProps}>
      {cachedChildrenRef.current}
    </Dialog>
  );
};

クローズアニメーションが完了した後にもダイアログの中身が見えていないだけで残ってしまわないか心配するかもしれませんが、そもそもMUIのDialogはアニメーションが完了したらコンポーネントを破棄する作りになっているのでrefに子要素が残っていても大丈夫です。

念の為 子コンポーネントでmountとunmountのログを出して確認しましたが、キチンとアニメーションが終了した後にunmountされていました。

終わりに

以上がダイアログが閉じるまでコンテンツをキャッシュしてクローズアニメーションを綺麗にさせる方法でした。実装自体はとてもシンプルですがこれがあるだけで表示フラグと表示する中身に関するデータをそれぞれ持たなくてもクローズアニメーションがキチンとされるようになるので実装をシンプルに出来て良いなと思いました。
最後に検証コードを以下に貼りますので、詳細のコードや動作を確認したい方はこちらをご参照ください。

GitHubで編集を提案

Discussion