🕌

【Next.js】フォームの値をコンポーネント間でやりとりする

2022/06/23に公開

Reactではフォームの実装方法が書いてあったけどNextの方には書いてなかったのでまとめます。

https://ja.reactjs.org/docs/forms.html

前提

項目 バージョン 確認方法
docker Docker version 20.10.14 コマンドで確認
Node node:14.17.0 Dockerfileで確認
Next.js "next": "12.1.4" package.json
yarn "yarn": "1.22.5 yarn -v で確認

コンポーネント間の通信(実装方針)

親コンポーネントと子コンポーネント間を通信する場合には実装方針は2つあるみたいです。

  • コンポーネント間の通信にはstateを共有する
  • コールバックで関数を渡す

今回はコールバック関数を渡す方法で実装します。

子コンポーネントの実装

ここからは親子コンポーネントを実装していきます。それぞれのコンポーネントとファイル名は以下の表の通りになります。

コンポーネント tsxファイル
親コンポーネント index.tsx
子コンポーネント SearchForm.tsx

まずは子コンポーネントとしてSearchForm.tsxを実装です。

inputタグにonChangeイベントで値が変更されたら、そのeventをコールバック関数で親コンポーネントに通知します。

以下のコードではonChangeイベントでonChangeCountryを呼び出します。

その他のpropsはonChangeCountry と同様のonChangePrice, onChangeShopAreaメソッドとセレクトボックスのoptionタグを生成します。

これで子コンポーネントで値が変更されたら、親コンポーネントに通知することができました。

SearchForm.tsx

import React from 'react';
import PropTypes from 'prop-types';

type Country = {
  value: string;
  label: string;
}
type Price = {
  value: string,
  label: string;
}
type ShopArea = {
  value: string,
  label: string;
};
interface SearchFormProps {
  countries: Country[];
  priceOptions: Price[];
  shopAreaOptions: ShopArea[];
  onChangeCountry: (event: any) => void;
  onChangePrice: (event: any) => void;
  onChangeShopArea: (event: any) => void;
}


const SearchForm = ({countries, priceOptions, shopAreaOptions, onChangeCountry, onChangePrice, onChangeShopArea}:SearchFormProps) => {
  return (
    <div className="px-24 pt-24 pb-40 bg-main max-w-6xl rounded-3xl mx-auto bg-human bg-no-repeat bg-right-bottom">
      <p className="text-5xl font-bold">自分に合った<br/>カフェインレスコーヒーを<br/>見つけよう</p>
      <div className="flex mt-20 justify-center">
        <div className="bg-evony-100 rounded-2xl py-4 pl-10 pr-6 mr-5">
          <select name="Country" id="Country" className="text-2xl font-medium" onChange={(e) => onChangeCountry(e)}>
            {countries.map((country) => 
              <option key={country.value} value={country.value}>{country.label}</option>
            )}
          </select>
        </div>
        <div className="bg-evony-100 rounded-2xl py-4 pl-12 pr-6 mr-5">
          <select name="Price" id="Price" className="text-2xl font-medium" onChange={(e) => onChangePrice(e)}>
            {
              priceOptions.map((priceOption)=> 
                <option key={priceOption.value} value={priceOption.value}>{priceOption.label}</option>
            )}
          </select>
        </div>
        <div className="bg-evony-100 rounded-2xl py-4 pr-6 pl-10">
          <select name="ShopArea" id="ShopArea" className="text-2xl font-medium" onChange={(e) => onChangeShopArea(e)}>
            {shopAreaOptions.map((shopAreaOption) => 
              <option key={shopAreaOption.value} value={shopAreaOption.value}>{shopAreaOption.label}</option>
            )}
          </select>
        </div>
      </div>
    </div>
  )
}

export default SearchForm

ここまでで子コンポーネントの実装は完了です。

親コンポーネントの実装

子コンポーネントで実装したSearchFormコンポーネントを親コンポーネントのindex.tsxにマウントします。

コンポーネント ファイル メソッド
親コンポーネント index.tsx onChangeCountry(コールバック関数)
子コンポーネント SearchForm.tsx onChange(イベント)

子コンポーネントのonChangeCountryメソッド が発火したら、 親コンポーネントのsetCountryを呼び出す ようにします。

その他のonChangePrice, onChangeShopAreaも同様です。

他のpropsはSearchFormの選択肢を表示するために適当なオブジェクトを渡しています。

inxex.tsx
import React, { useState, useEffect } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import Header from '../stories/components/molecules/Header';
import Footer from '../stories/components/molecules/Footer';
import SearchResult from '../stories/components/templates/SearchResult';
import SearchForm from '../stories/components/molecules/SearchForm';
import coffeeShopPng from '../public/coffee_shop_1.png';

const Home: NextPage = () => {
  const [country, setCountry] = useState("");
  const [price, setPrice] = useState("");
  const [shopArea, setShopArea] = useState("");
  const [coffeeShops, setCoffeeShops] = useState(coffeeShopList);
  useEffect(() => {
    // すべて選択項目が変わっていなければ、初期化
    if((country === "" || Number(country) === 0)
    && (price === "" || Number(price) === 0)
    && (shopArea === "" || Number(shopArea) === 0)){
      setCoffeeShops(coffeeShopList);
      return;
    }

    if (Number(country) !== 0) {
      const result = coffeeShops.filter((coffeeShop) => {
        return coffeeShop.country.id === Number(country);
      })
      setCoffeeShops(result.length ? result : []);
    }

    if (Number(price) !== 0) {
      const result = coffeeShops.filter((coffeeShop) => {
        return coffeeShop.price.id === Number(price);
      })
      setCoffeeShops(result.length ? result : []);
    }

    if (Number(shopArea) !== 0) {
      const result = coffeeShops.filter((coffeeShop) => {
        return coffeeShop.shopArea.id === Number(shopArea);
      })
      setCoffeeShops(result.length ? result : []);
    }
    return;
  
  }, [country, price, shopArea]);
  return (
    <>
      <Head>
        <title>CafeeLessMore</title>
        <meta name="description" content="CafeeLessMore" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Header/>
      <main className="bg-evony-100">
        <div className="mt-16 mb-40">
          <SearchForm
            countries={countries}
            priceOptions={priceOptions}
            shopAreaOptions={shopAreaOptions}
            onChangeCountry={(e) => setCountry(e.target.value)}
            onChangePrice={(e) => setPrice(e.target.value)}
            onChangeShopArea={(e) =>setShopArea(e.target.value)}
          />
        </div>
        <div className="mb-40">
          <SearchResult coffeeShops={coffeeShops} />
        </div>
      </main>
      <Footer/>
    </>
  )
}

実際にうごいているのはこちらです。

https://whaleshark.vercel.app/

参考URL

https://ja.reactjs.org/docs/lifting-state-up.html

https://ja.reactjs.org/docs/hooks-reference.html

https://ja.reactjs.org/docs/faq-functions.html

宣伝(たまにライブコーディングします)

https://twitter.com/DotMiyama/status/1539267468463833088?s=20&t=AhMmrJ25A43JNXr7rN3i9A

Oriental Coffee Ventures

Discussion