問題

  • FullCalendar と 同じページに shadcn Dialog を組み込み、 shadcn (Radix UI) Dialog の open ステートを useState で管理していると、 ダイアログの open/close 時に FullCalendar が再レンダリングされてしまう。

    • GoogleCalendarのイベントや、独自イベントがある場合、再レンダリングされると、 イベントが消えてしまう。

解決方法

  • ref, forwardRefs, useImperativeHandle を使って、再レンダリングを防ぐ。

以下詳細

まずは Dialog側

import {
  Ref,
  useMemo,
  useCallback,
  useState,
  forwardRef,
  useImperativeHandle,
} from 'react';

import { Button } from '@/components/ui/button';
import {
  Dialog,
  DialogContent,
} from '@/components/ui/dialog';

type SampleProps = {
};

export interface SampleDialogRefInterface {
  open: () => void;
  close: () => void;
}

const SampleDialogButtonInner = (_: SampleProps, ref: Ref<SampleDialogRefInterface>) => {
  const [isOpen, setIsOpen] = useState(false);

  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
  }));

  return (
    <Dialog open={isOpen} onOpenChange={setIsOpen}>
      {/* 外部からOpenしたいので、DialogTriggerは使わない */}
      {/*
      <DialogTrigger asChild>
        <Button>ボタン</Button>
      </DialogTrigger>
        */}
      <DialogContent className='h-[90%] overflow-hidden'>
        {/* 省略 */}
      </DialogContent>
    </Dialog>
  );
  // }
};
const SampleDialogButton = forwardRef(SampleDialogButtonInner);
export default SampleDialogButton;
  • SampleProps
    • 親側から渡す props
  • SampleDialogRefInterface
    • ダイアログとして公開したいメソッドを定義する
  • _: SampleProps
    • props は使わないので、_ で受け取る。propsがないとエラー
  • ref: Ref<SampleDialogRefInterface>
    • ref を受け取る
  • const [isOpen, setIsOpen] = useState(false);
    • ダイアログの open/close ステートを useState で管理
  • <Dialog open={isOpen} onOpenChange={setIsOpen}>
    • isOpen, setIsOpen を Dialog の open, onOpenChange で紐付け
  • useImperativeHandle
    • ref として公開して、外部から呼ばれた場合の動作を定義
    • Dialogの開閉を外部から制御できるようにする
  • const SampleDialogButton = forwardRef(SampleDialogButtonInner)
    • forwardRef で ref を受け取るコンポーネントを作成

FullCalendar 側(親側)

import { EventClickArg } from '@fullcalendar/core';
import FullCalendar from '@fullcalendar/react';
import { useRef, useCallback } from 'react';

import SampleDialogButton, {
  SampleDialogRefInterface,
} from '@/app/_components/SampleDialogButton';

export default function Calendar() {
  const sampleDialogRef = useRef<SampleDialogRefInterface>(null);

  const onDateClick = useCallback(
    (arg: DateClickArg) => {
      const target = arg.dateStr;
      sampleDialogRef.current.open();
    },
    [],
  );
  const onEventClick = useCallback((arg: EventClickArg) => {
    if (!sampleDialogRef.current) {
      return;
    }
    sampleDialogRef.current.open();
  }, []);

  return (
    <>
      <SampleDialogButton ref={sampleDialogRef} />
      <FullCalendar
        dateClick={onDateClick}
        eventClick={onEventClick}
      />
    </>
  );
}
  • const sampleDialogRef = useRef<SampleDialogRefInterface>(null);
    • Dialog側の公開IF型の ref を作成
  • const onDateClick = useCallback(
    • FullCalendar の dateClick イベントハンドラ
    • クリックされた日付を取得し、ダイアログを開く
  • const onEventClick = useCallback((arg: EventClickArg) => {
    • FullCalendar の eventClick イベントハンドラ
    • クリックされたイベントを取得し、ダイアログを開く
  • <SampleDialogButton ref={sampleDialogRef} />
    • Dialogコンポーネントをレンダリング
    • ref に sampleDialogRef を渡す

Refs