Event HandlerとuseStateについて紹介していきます。
Event Handlerには、onClick、onChange、onSubmitなどがあります。
ユーザーがボタンを押下する、何か画面に入力するなどの操作をトリガーとして、定義したイベントを実行する仕組みになります。
useStateと組み合わせて使うことがあるため、一緒に紹介します。
1.前回の記事について
こちらの記事をまだご覧になってない方は、先に確認頂くことをおすすめします。
前回の記事ではpropsの受け渡しについて解説しました。
本記事では、前回記事で使ったコードを元に変更・追加をしていきます。
またEvent HandlerとuseStateの解説がメインになりますが、やっていること自体は、本記事単体だけでもわかるように心がけたいと思います。
2.Event Handlerについて
簡単なコードの例を見て紹介致します。
ExpenseItem.js
import './ExpenseItem.css'; import Card from '../UI/Card'; import ExpenseDate from './ExpenseDate'; export default function ExpenseItem(props) { const changeHandler = () => { console.log('update'); } return ( <Card className='expense-item'> <ExpenseDate date={props.date} /> <div className="expense-item__description"> <h2>{props.title}</h2> <div className="expense-item__price">{props.amount}</div> </div> <button onClick={changeHandler}>change title</button> </Card> ) }
日付表示の箇所をExpenseDateという別のcomponentに用意しています。
Event Handlerの動作確認としてchangeHandlerというarrow functionを作ります。
const changeHandler = () => { console.log('update'); }
またbuttonにonClickを追加して、ボタンが押下されたときに、changeHandlerが実行されるようにします。
<button onClick={changeHandler}>change title</button>
※ {changeHandler()}としてしまうと、componentが読み込まれたときにchangeHandlerが実行されてしまうので、ポインターとして指し示すようにします。
ExpenseDate.js
import './ExpenseDate.css'; function ExpenseDate(props) { const month = props.date.toLocaleString('en-US', { month: 'long' }); const day = props.date.toLocaleString('en-US', { day: '2-digit' }); const year = props.date.getFullYear(); return ( <div className="expense-date"> <div className="expense-date__month">{month}</div> <div className="expense-date__year">{year}</div> <div className="expense-date__day">{day}</div> </div> ); } export default ExpenseDate;
ExpenseDate.css
.expense-date { display: flex; flex-direction: column; width: 5.5rem; height: 5.5rem; border: 1px solid #ececec; background-color: #2a2a2a; color: white; border-radius: 12px; align-items: center; justify-content: center; } .expense-date__month { font-size: 0.75rem; font-weight: bold; } .expense-date__year { font-size: 0.75rem; } .expense-date__day { font-size: 1.5rem; font-weight: bold; }
この状態で動作確認をしてみます。
「change title」ボタンを押したときにconsoleに「update」が表示されます。
3.React hook useState とは
React hookの1つであるuseStateについて紹介致します。
useStateを利用することで、ある値の状態(state)を管理することができます。
デフォルトでは、以下のような通常の変数は、再評価(componentの再読み込み)をトリガーしません。
let title=props.title;
再評価を実行する必要があることをReactに伝えるには、Reactライブラリ{useState}をインポートする必要があります。
以下を追加することでuseStateがcomponent内で使えるようになります。
import { useState } from 'react';
component関数の内部では、useState(React Hook)を呼び出すだけです。
Reactフックは「use」で認識され、関数の外で呼び出すことはできません。
※ネストされた関数内でuseStateを呼び出すべきではないことに注意が必要です
- titleは状態管理した変数
- setTitleは特別なメソッドで、setTitleが実行されたときにcomponentを再度レンダリング
- 慣例的に「set」+変数名でlower camelで記載
- useStateの()内に初期値を記載
4.React hook useState の基本的な使い方
useStateを使って、ExpenseItem component内のtitleを更新してみます。
const [title, setTitle] = useState(props.title);
titleの初期値には、props.titleを指定します。
「change title」ボタンを押したときにchangeHandlerを実行すると、setTitleによりtitleが更新されるようになります。
またJSXコード内には以下のように記載します。
<h2>{title}</h2>
表示されているItemの内の1つのボタンをクリックすると、そのItemのタイトルのみ「update」に変わります。
また他のItemのtitleは更新されません。
5.useStateとEvent Handlerを組み合わせた使い方
入力フォームを追加して、入力値が変わったときの状態管理を紹介します。
ExpenseForm.js
import { useState } from 'react'; import './ExpenseForm.css'; const ExpenseForm = () => { const [title, setTitle] = useState(''); const titleChangeHandler = (event) => { console.log(event.target.value); }; return ( <form> <div className='new-expense__controls'> <div className='new-expense__control'> <label>Title</label> <input type='text' onChange={titleChangeHandler} /> </div> <div className='new-expense__control'> <label>Price</label> <input type='number' min='0.01' step='0.01' /> </div> <div className='new-expense__control'> <label>Date</label> <input type='date' min='2019-01-01' max='2022-12-31' /> </div> </div> <div className='new-expense__actions'> <button type='submit'>+ Add</button> </div> </form> ); } export default ExpenseForm;
この状態では、まだExpenseForm.jsはuseStateを定義しただけで使用していません。
titleChangeHandlerというarrow functionを定義しています。
const titleChangeHandler = (event) => { console.log(event.target.value); }
inputの状態が変わったときにtitleChangeHandlerが実行されます。
event.targetはJavaScriptの記述で、対象のDOM要素を取得できます。
<input type='text' onChange={titleChangeHandler} />
ExpenseForm.css
.expense-item { display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); padding: 0.5rem; margin: 1rem 0; border-radius: 12px; background-color: #4b4b4b; } .expense-item__description { display: flex; flex-direction: column; gap: 1rem; align-items: flex-end; flex-flow: column-reverse; justify-content: flex-start; flex: 1; } .expense-item h2 { color: #3a3a3a; font-size: 1rem; flex: 1; margin: 0 1rem; color: white; } .expense-item__price { font-size: 1rem; font-weight: bold; color: white; background-color: #40005d; border: 1px solid white; padding: 0.5rem; border-radius: 12px; } @media (min-width: 580px) { .expense-item__description { flex-direction: row; align-items: center; justify-content: flex-start; flex: 1; } .expense-item__description h2 { font-size: 1.25rem; } .expense-item__price { font-size: 1.25rem; padding: 0.5rem 1.5rem; } }
NewExpense.js
import React from 'react'; import ExpenseForm from './ExpenseForm'; import './NewExpense.css'; const NewExpense = () => { return ( <div className='new-expense'> <ExpenseForm /> </div> ); }; export default NewExpense;
NewExpense.css
.new-expense { background-color: #a892ee; padding: 1rem; margin: 2rem auto; width: 50rem; max-width: 95%; border-radius: 12px; text-align: center; box-shadow: 0 1px 8px rgba(0, 0, 0, 0.25); } .new-expense button { font: inherit; cursor: pointer; padding: 1rem 2rem; border: 1px solid #40005d; background-color: #40005d; color: white; border-radius: 12px; margin-right: 1rem; } .new-expense button:hover, .new-expense button:active { background-color: #510674; border-color: #510674; } .new-expense button.alternative { color: #220131; border-color: transparent; background-color: transparent; } .new-expense button.alternative:hover, .new-expense button.alternative:active { background-color: #ddb3f8; }
Component Treeは以下のような感じになります。
ExpenseApp にNewExpense componentを追加して、画面を表示してみます。
titleに文字を入力すると、consoleに値が出力され、フォーム値に変化があった際にevent.target.valueで値を取得できていることがわかります。
次にtitleChangeHandlerの中身を以下のように変更します。
const titleChangeHandler = (event) => { // console.log(event.target.value); setTitle(event.target.value); console.log('title: ',title); }
画面をリロードして、titleにもう一度文字を入力してみます。
文字を入力していくと、setTitleが実行された直後、titleの初期値から1つ前の状態まで値がログに出力されていることがわかります。
6.入力フォームにsubmit処理を追加する
入力フォームにsubmit処理を追加して、一覧に表示していきます。
ExpenseForm.js
const ExpenseForm = (props) => { const [title, setTitle] = useState(''); const [price, setPrice] = useState(''); const [date, setDate] = useState(''); const titleChangeHandler = (event) => { setTitle(event.target.value); } const priceChangeHandler = (event) => { setPrice(event.target.value); } const dateChangeHandler = (event) => { setDate(event.target.value); } const submitHandler = (event) => { event.preventDefault(); const expenseData = { title:title, amount:price, date:new Date(date) }; console.log(expenseData); } return ( <form onSubmit={submitHandler}> <div className='new-expense__controls'> <div className='new-expense__control'> <label>Title</label> <input type='text' onChange={titleChangeHandler} /> </div> <div className='new-expense__control'> <label>Price</label> <input type='number' min='0.01' step='0.01' onChange={priceChangeHandler} /> </div> <div className='new-expense__control'> <label>Date</label> <input type='date' min='2019-01-01' max='2022-12-31' onChange={dateChangeHandler} /> </div> </div> <div className='new-expense__actions'> <button type='submit'>+ Add</button> </div> </form> ); } export default ExpenseForm;
submitHandlerを追加して、入力した値が取得できるかを確認します。
const submitHandler = (event) => { event.preventDefault(); const expenseData = { title:title, amount:price, date:new Date(date) }; console.log(expenseData); }
event.preventDefault()を追加することで、フォームのボタンがクリックされたときに、ブラウザによるフォームのリクエスト送信とページのリロードがされなくなります。
この処理を挟むことで、JSの処理を続けて行うことができます。
expenseDataをconsoleに出力すると、フォームのボタンが押されたときに入力された値が取得できていることがわかります。
consoleで確認ができたら、submitHandlerを以下のように変更します。
const submitHandler = (event) => { event.preventDefault(); const expenseData = { id: Math.random().toString(), title:title, amount:price, date:new Date(date) }; props.onAddExpense(expenseData); }
propsからevent handlerとしてonAddExpenseを受け取り、expenseDataを渡します。
NewExpense.js
const NewExpense = (props) => { return ( <div className='new-expense'> <ExpenseForm onAddExpense={props.onAddExpense} /> </div> ); };
カスタム event handlerとしてonAddExpenseをJSXコードに追加します。
親componentであるExpenseAppから、props.onAddExpenseを渡します。
ExpenseApp.js
import { useState } from "react"; import ExpenseItem from "../components/Expenses/ExpenseItem"; import NewExpense from "../components/Expenses/NewExpense"; const DUMMY_DATA = [ {id:'1', title: 'Car Insurance', amount:94.12, date: new Date(2022,4,12)}, {id:'2', title: 'Book', amount:14.31, date: new Date(2022,5,30)}, {id:'3', title: 'Bag', amount:114.76, date: new Date(2022,6,19)}, {id:'4', title: 'Toy', amount:24.51, date: new Date(2022,7,3)} ]; const ExpenseApp = () => { const [expenses, setExpenses] = useState(DUMMY_DATA); const addExpenseHandler = (enteredExpense) => { setExpenses((prevExpense) => { return [enteredExpense, ...prevExpense]; }); } return ( <div className="ExpenseApp"> <NewExpense onAddExpense={addExpenseHandler} /> {expenses.map((expense) => ( <ExpenseItem key={expense.id} title={expense.title} amount={expense.amount} date={expense.date} /> ))} </div> ); }; export default ExpenseApp;
ExpenseAppの変更点
const [expenses, setExpenses] = useState(DUMMY_DATA); const addExpenseHandler = (enteredExpense) => { setExpenses((prevExpense) => { return [enteredExpense, ...prevExpense]; }); }
useStateを使って、expenses(data)の状態管理をしています。また初期値として、componentの外側に定義したDUMMY_DATAを設定しています。
addExpenseHandlerでは、引数としてenteredExpenseを子componentから受け取ります。
prevExpenseは前の状態(スナップショット)になります。
…prevExpense
spread operator(スプレッド構文)を利用することで、objectの内容を展開して、新しい入力値enteredExpenseをリストに詰めて、setExpensesに設定します。
<NewExpense onAddExpense={addExpenseHandler} />
カスタム event handlerとしてonAddExpenseを追加して、addExpenseHandlerをポインターとして設定します。
{expenses.map((expense) => ( <ExpenseItem key={expense.id} title={expense.title} amount={expense.amount} date={expense.date} /> ))}
こちらは、今までExpenseItemを1つずつ直接書いていたのを、mapメソッドを使って、expenses(リスト)の中身を繰り返し表示するように変更しています。
mapメソッドを使用する上で、keyを追加しています。
keyを追加しなくても動作はするものの、consoleに警告が出るのと、バグにつながるため、ユニークな値としてMath.random()でidを設定しています。
フォームに入力した内容が一覧に追加表示されたことを確認できました。
7.まとめ
Event HandlerとuseStateの使い方について紹介いたしました。
- Event HandlerをJSXコード内で呼び出す際は、ポインターとして指し示すことでcomponentが読み込まれても自動的に実行されない
- component内部では、useState(React Hook)を呼び出すことができる
- ネストされた関数内でuseStateを呼び出すべきではない
- useStateで変数(string, list, object etc)の状態管理が可能
- event.targetにて、対象のDOM要素を取得可能
- event.preventDefault()を追加することで、JS処理を継続して行うことが可能
- カスタムevent handlerをpropsで受け取ることが可能