0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Contextを使った値の受け渡し

0
Posted at

この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。

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.実装内容

ModalContexts.tsx
"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が持っている変数一式を返却するので呼び出し側で使えるようになります。

app/layout.tsx
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として渡しているものを受け取ることができるようになります。

DateAndEventsComponent.tsx(子要素)
"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に囲まれた要素なので、エラーは発生せずに変数を取得できています。

ここまで読んでいただきありがとうございました。
実装の一助になれば幸いです。

2.参考にしたページ

【React】UseContext、createContextの使い方

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?