この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。
0.前提条件
この記事は以下の技術スタックを使用してGoogle Calendarを模したアプリを作った時に遭遇したエラーを解説するものです。
技術スタック
React ver 19.2.3
Next.js ver 16.1.6
TypeScript ver 5
tailwindCSS ver 4.1.8
完成したアプリはVercelにデプロイして閲覧できるようにしました。
1.実装の目的
コンポーネント間で値を共有したい場合、propsとして渡してあげる方法がありますが、これは親子関係にある時だけ使える方法です。
親子同士ではないコンポーネント間で値を受け渡す方法として以下の方法などが挙げられます。
- Linkコンポーネントなどを使ってページ遷移する時にクエリパラメーターとして渡す
- 上記に近いがパスパラメーターとして渡す
- Contextを使って値を受け渡す
- サーバーのデータベースから取得する
- グローバル state ライブラリ(Zustand / Redux など)
- ローカルストレージ / セッションストレージ
- 親のコールバック経由で「子 → 親 → 別の子」へリフトアップ
今回はサーバーのデータベースは実装しておらず、複数のstate変数やset関数などが対象、といった理由からContextを使った受け渡しで実装することにしました。
1.実装内容
"use client";
import { createContext, useContext, useState } from "react";
import { useError } from "./ErrorContexts";
import { eventTypeZod } from "@/types/eventType";
import { findCurrentEvent } from "@/utils/findCurrentEvent";
// 各コンポーネントに渡す値の型定義
export type ModalContextType = {
events: eventTypeZod[];
setEvents: React.Dispatch<React.SetStateAction<eventTypeZod[]>>;
showEventModal: boolean;
showEventChangeModal: boolean;
designatedDate: Date;
designatedId: number;
openModalHandler: (day: Date) => void;
openChangeModalHandler: (day: Date, id: number) => void;
closeModalHandler: () => void;
closeChangeModalHandler: (signal: boolean) => void;
updatedTitle: string;
setUpdatedTitle: React.Dispatch<React.SetStateAction<string>>;
currentEvent: eventTypeZod;
};
// 意味のあるデフォルトがない時はとりあえずnullで対応
const ModalContext = createContext<ModalContextType | null>(null);
export const ModalProvider = ({ children }: { children: React.ReactNode }) => {
const [events, setEvents] = useState<eventTypeZod[]>([]);
const [showEventModal, setShowEventModal] = useState<boolean>(false);
const [showEventChangeModal, setShowEventChangeModal] =
useState<boolean>(false);
const [designatedDate, setDesignatedDate] = useState<Date>(new Date());
const [designatedId, setDesignatedId] = useState<number>(0);
const currentEvent: eventTypeZod = findCurrentEvent(designatedId, events);
const [updatedTitle, setUpdatedTitle] = useState<string>(
currentEvent ? currentEvent.title : "",
);
// エラーメッセージの管理
const { errorMessage, setErrorMessage } = useError();
// イベント作成のモーダルを表示する
const openModalHandler = (day: Date) => {
setShowEventModal(true);
setDesignatedDate(day);
};
// イベント作成のモーダルを閉じる
const closeModalHandler = () => {
setShowEventModal(false);
setErrorMessage([]);
};
// イベント変更・削除のモーダルを表示する
const openChangeModalHandler = (day: Date, id: number) => {
setShowEventChangeModal(true);
setDesignatedDate(day);
setDesignatedId(id);
setErrorMessage([]);
};
// イベント変更・削除のモーダルを閉じる。変更ボタンを押していない場合はタイトルの内容を編集前に戻す
const closeChangeModalHandler = (signal: boolean) => {
setShowEventChangeModal(false);
if (signal) {
setUpdatedTitle(currentEvent.title);
}
};
// childrenを囲うことでchildrenに対してvalueを受け渡す
return (
<ModalContext.Provider
value={{
events,
setEvents,
showEventModal,
showEventChangeModal,
designatedDate,
designatedId,
openModalHandler,
openChangeModalHandler,
closeModalHandler,
closeChangeModalHandler,
updatedTitle,
setUpdatedTitle,
currentEvent,
}}
>
{children}
</ModalContext.Provider>
);
};
// Providerの外でuseContext(ModalContext)を呼ぶとctxはnullになるので、エラーで潰しておく。
export const useModal = () => {
const ctx = useContext(ModalContext);
if (ctx === null)
throw new Error("useModal must be used within ModalProvider");
return ctx;
};
受け渡す対象のstate変数、set関数などを定義しています。
モーダルの表示・非表示を管理する処理を実装したものです。
const ModalContext = createContext<ModalContextType | null>(null)
の部分ではデフォルト値としてnullを設定しています。
これはModalContext.Providerで囲んでいないところでModalContextを呼ぶとcreateContextに渡したデフォルト値(null)を返却させて、Providerの外で使っていることを判別しやすくしています。
useModalのところでは前述の通りModalContext.Providerで囲んでいないところでModalContextを呼ぶとデフォルト値のnullが返却されるため、エラーを発生させて開発者に誤った場所で呼び出していることを知らせる目的があります。
エラーが発生しない(返り値がnullではない)場合、ModalProviderが持っている変数一式を返却するので呼び出し側で使えるようになります。
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ModalProvider } from "@/contexts/ModalContexts";
import { ErrorProvider } from "@/contexts/ErrorContexts";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<ErrorProvider>
<ModalProvider>{children}</ModalProvider>
</ErrorProvider>
</body>
</html>
);
}
Next.jsではapp/layout.tsxで{children}をContextのProviderで囲むことにより、全ての子要素でModalContextからvalueとして渡しているものを受け取ることができるようになります。
"use client";
import React from "react";
import { checkEvents } from "@/utils/checkEvents";
import EventCreateModal from "./EventCreateModal";
import EventEditAndDeleteModal from "./EventEditAndDeleteModal";
import { useModal } from "@/contexts/ModalContexts";
import { compareDates } from "@/utils/compareDates";
import { getDate } from "date-fns";
import { today } from "@/constants/calendar";
type PropsType = {
date: Date;
month: string;
isMonth: boolean;
};
const DataAndEventsComponent = ({ date, month, isMonth }: PropsType) => {
const {
events,
setEvents,
showEventModal,
showEventChangeModal,
designatedDate,
designatedId,
openModalHandler,
openChangeModalHandler,
closeModalHandler,
closeChangeModalHandler,
} = useModal();
return (
<>
<div
suppressHydrationWarning
className={
isMonth
? "flex flex-col border border-gray-300 text-center"
: "flex h-screen flex-col border border-gray-300 text-center"
}
onClick={() => openModalHandler(date)}
role="button"
tabIndex={0}
>
<div className="p-1">
<span className={compareDates(today, date, month)}>
{getDate(date)}
</span>
</div>
<div className="flex flex-col">
{events &&
checkEvents({
events,
date,
}).map((event) => (
<button
suppressHydrationWarning
key={event.id}
className="rounded-md bg-blue-300 text-black"
onClick={(e) => {
e.stopPropagation();
openChangeModalHandler(date, event.id);
}}
>
{event.title}
</button>
))}
</div>
</div>
{showEventModal && (
<EventCreateModal
show={showEventModal}
close={closeModalHandler}
date={designatedDate}
setEvents={setEvents}
events={events}
/>
)}
{showEventChangeModal && (
<EventEditAndDeleteModal
show={showEventChangeModal}
close={closeChangeModalHandler}
date={designatedDate}
id={designatedId}
setEvents={setEvents}
events={events}
/>
)}
</>
);
};
export default DataAndEventsComponent;
呼び出し側ではuseModalと呼び出すことでcreateContextしている側で実装した変数などを実際に呼び出しています。
DataAndEventsComponentコンポーネントはModalProviderに囲まれた要素なので、エラーは発生せずに変数を取得できています。
ここまで読んでいただきありがとうございました。
実装の一助になれば幸いです。