【第8回】親子コンポーネントを使ってウィンドウを閉じる機能を作成

おけまるの投資開発室

今回は、株アプリの銘柄詳細画面を「×ボタン」で閉じる機能を作りました。

閉じるだけなら簡単そうに見えますが、この株アプリでは、画面全体を管理するコンポーネントと、銘柄詳細を表示するコンポーネントを分けています。

そのため、親コンポーネントが持っている表示状態を、子コンポーネントにある「×ボタン」から変更する必要がありました。

▼今回の開発動画はこちら
https://youtu.be/KMu8DWJBvVA

今回作りたかった機能

現在作っている株アプリは、画面を大きく3つに分けています。

  • 左側:銘柄の検索条件

  • 中央:銘柄一覧

  • 右側:選択した銘柄の詳細情報

今回作りたかったのは、右側の銘柄詳細に「×ボタン」を設置し、クリックすると詳細画面が閉じる機能です。

株アプリでは、ページ全体をpage.tsx、銘柄詳細をStockDetail.tsxという別々のコンポーネントにしています。

それぞれの役割は、次のようになっています。

  • page.tsx:ページ全体と銘柄詳細の表示状態を管理する親コンポーネント

  • StockDetail.tsx:銘柄詳細と「×ボタン」を表示する子コンポーネント

親コンポーネントで表示状態を管理する

まず、親コンポーネントのpage.tsxで、銘柄詳細が開いているかどうかを管理します。

const [isDetailOpen, setIsDetailOpen] = useState(true);

isDetailOpenには、銘柄詳細が現在表示されているかどうかが入ります。

  • true:銘柄詳細を表示する

  • false:銘柄詳細を表示しない

setIsDetailOpenは、isDetailOpenの値を変更するための関数です。

初期値をtrueにしているため、最初にページを開いたときは銘柄詳細が表示されます。

trueのときだけStockDetailを表示する

page.tsxでは、isDetailOpentrueのときだけStockDetailを表示しています。

{isDetailOpen && (
  <StockDetail onClose={() => setIsDetailOpen(false)} />
)}

この書き方では、isDetailOpentrueの場合にだけ、右側の銘柄詳細が表示されます。

反対に、isDetailOpenfalseになると、StockDetailは表示されなくなります。

今回の機能を単純な日本語にすると、次のような処理です。

もし銘柄詳細が開いているなら、StockDetailを表示する

親から子へ閉じる処理を渡す

表示状態を変更するsetIsDetailOpenは、親コンポーネントのpage.tsxにあります。

しかし、実際にクリックする「×ボタン」は、子コンポーネントのStockDetail.tsxにあります。

子コンポーネントから、親コンポーネントにある状態を直接変更することはできません。

そこで、親コンポーネントから子コンポーネントへ、銘柄詳細を閉じるための処理を渡します。

<StockDetail onClose={() => setIsDetailOpen(false)} />

このコードでは、onCloseという名前で、次の処理をStockDetailへ渡しています。

() => setIsDetailOpen(false)

この処理が実行されると、isDetailOpenfalseになり、銘柄詳細が非表示になります。

親コンポーネントから子コンポーネントへ値や関数を渡すときに使うのがpropsです。

propsの型を定義する

次に、子コンポーネントのStockDetail.tsxで、親から受け取るpropsの型を定義します。

type StockDetailProps = {
  onClose: () => void;
};

今回、子コンポーネントが受け取るのはonCloseという関数です。

onClose: () => void;

これは、次のような意味です。

  • 関数名:onClose

  • 引数:なし

  • 戻り値:なし

voidは、この関数が値を返さないことを表しています。

型を定義することで、StockDetailがどのようなpropsを受け取るのかが分かりやすくなります。

子コンポーネントでpropsを受け取る

StockDetailでは、親から渡された値をpropsとして受け取ります。

export default function StockDetail(props: StockDetailProps) {

propsの型には、先ほど作成したStockDetailPropsを指定しています。

これによって、propsの中にonCloseという関数が入っていることをTypeScriptが認識できます。

今回のpropsをイメージすると、次のような状態です。

props
└─ onClose:銘柄詳細を閉じるための関数

「×ボタン」からonCloseを実行する

銘柄詳細の「×ボタン」には、次のようにクリック時の処理を設定しました。

<button type="button" onClick={props.onClose}>
  ×
</button>

ボタンをクリックすると、props.onCloseが実行されます。

ただし、閉じる処理そのものをStockDetail.tsxの中に書いているわけではありません。

実際に実行されるのは、親コンポーネントから渡された次の処理です。

() => setIsDetailOpen(false)

その結果、親コンポーネントのisDetailOpenfalseになり、StockDetailが非表示になります。

×ボタンを押したときの流れ

今回の処理を順番に整理すると、次のようになります。

  1. 親のpage.tsxisDetailOpenを管理する

  2. 最初はtrueなのでStockDetailが表示される

  3. 親から子へonCloseという関数を渡す

  4. 子の「×ボタン」をクリックする

  5. props.onCloseが実行される

  6. 親のsetIsDetailOpen(false)が実行される

  7. isDetailOpenfalseになる

  8. StockDetailが非表示になる

今回理解が難しかったのは、子コンポーネントが親の状態を直接変更するのではないという点です。

親が状態を変更する関数を子へ渡し、子はボタンが押されたときに、その関数を実行します。

詳細を閉じた後のレイアウトも変更する

銘柄詳細を非表示にするだけでは、右側に詳細画面用の空きスペースが残ってしまう可能性があります。

そこで、page.tsxでは、isDetailOpenの状態に応じてmainに指定するCSSクラスも切り替えています。

<main
  className={`${styles.mainLayout} ${
    !isDetailOpen ? styles.detailClosed : ""
  }`}
>

通常は、mainLayoutが適用されます。

styles.mainLayout

銘柄詳細を閉じてisDetailOpenfalseになると、detailClosedも追加されます。

styles.detailClosed

条件部分だけを見ると、次のようになっています。

!isDetailOpen ? styles.detailClosed : ""

!isDetailOpenは「isDetailOpenがfalseかどうか」という意味です。

そのため、処理は次のようになります。

  • 詳細が開いている:mainLayoutだけを適用

  • 詳細が閉じている:mainLayoutdetailClosedを適用

これによって、銘柄詳細を閉じるだけでなく、残った検索条件と銘柄一覧に合わせて画面のレイアウトも変更できます。

page.tsxの完成コード

親コンポーネントのpage.tsxは、次のようになりました。

"use client";

import { useState } from "react";

import Header from "@/components/Header/Header";
import SearchPanel from "@/components/SearchPanel/SearchPanel";
import StockList from "@/components/StockList/StockList";
import StockDetail from "@/components/StockDetail/StockDetail";

import styles from "./page.module.css";

export default function Home() {
  const [isDetailOpen, setIsDetailOpen] = useState(true);

  return (
    <>
      <Header />

      <main
        className={`${styles.mainLayout} ${
          !isDetailOpen ? styles.detailClosed : ""
        }`}
      >
        <SearchPanel />
        <StockList />

        {isDetailOpen && (
          <StockDetail onClose={() => setIsDetailOpen(false)} />
        )}
      </main>
    </>
  );
}

このファイルでは、主に次の処理を行っています。

  • 銘柄詳細の表示状態を管理する

  • 銘柄詳細が開いているときだけStockDetailを表示する

  • 銘柄詳細を閉じる関数をStockDetailへ渡す

  • 詳細を閉じたときにレイアウトを変更する

StockDetail.tsxへ追加したコード

StockDetail.tsxには、propsの型定義と「×ボタン」を追加しました。

type StockDetailProps = {
  onClose: () => void;
};

export default function StockDetail(props: StockDetailProps) {
  return (
    <section className={styles.stockDetail}>
      <button type="button" onClick={props.onClose}>
        ×
      </button>

      {/* 銘柄詳細の内容 */}
    </section>
  );
}

実際のStockDetail.tsxには、銘柄情報やタブを切り替える処理もあります。

しかし、今回作った「閉じる機能」で重要なのは、次の3か所です。

type StockDetailProps = {
  onClose: () => void;
};
export default function StockDetail(props: StockDetailProps) {
<button type="button" onClick={props.onClose}>
  ×
</button>

親からonCloseを受け取り、「×ボタン」がクリックされたときに実行しています。

今回理解できたこと

今回の開発では、次のことを確認できました。

  • useStateで銘柄詳細の表示状態を管理する

  • truefalseでコンポーネントの表示・非表示を切り替える

  • ページ全体を親、銘柄詳細を子コンポーネントとして分ける

  • 親コンポーネントで状態を管理する

  • propsを使って親から子へ関数を渡す

  • 子のボタンから、親から受け取った関数を実行する

  • TypeScriptでpropsの型を定義する

  • 詳細を閉じた状態に合わせてCSSクラスを切り替える

今回のポイントは、「子から親の状態を直接変更する」のではなく、「親から状態を変更する関数を受け取り、子で実行する」という部分でした。

まとめ

今回は、株アプリの銘柄詳細画面を「×ボタン」で閉じる機能を作りました。

親コンポーネントのpage.tsxisDetailOpenを管理し、子コンポーネントのStockDetail.tsxonCloseという関数を渡しています。

子コンポーネントの「×ボタン」からprops.onCloseを実行することで、親のisDetailOpenfalseになり、銘柄詳細が非表示になります。

さらに、詳細を閉じたときにはdetailClosedというCSSクラスを追加し、画面のレイアウトも変更しています。

親コンポーネントと子コンポーネントの関係は最初は分かりにくかったですが、処理を一つずつ分けて考えることで、株アプリ本体へ反映できました。

次回も、株アプリの機能を作りながらReactやTypeScriptへの理解を深めていきます。

▼実際の操作やエラーを含む動画はこちら
https://youtu.be/KMu8DWJBvVA

コメント

タイトルとURLをコピーしました