🕌
【Next.js】フォームの値をコンポーネント間でやりとりする
Reactではフォームの実装方法が書いてあったけどNextの方には書いてなかったのでまとめます。
前提
項目 | バージョン | 確認方法 |
---|---|---|
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/>
</>
)
}
実際にうごいているのはこちらです。
参考URL
宣伝(たまにライブコーディングします)
Discussion