インタラクティビティの追加
画面上の要素には、ユーザの入力に反応して更新されていくものがあります。例えば、イメージギャラリをクリックするとアクティブなイメージが切り替わります。React では、時間の経過とともに変化するデータのことを state と呼びます。任意のコンポーネントに state を追加することができ、必要に応じて更新することができます。この章では、インタラクションを処理し、state を更新し、時間の経過とともに異なる出力を表示するコンポーネントの作成方法について学びます。
この章で学ぶこと
イベントへの応答
React では、JSX にイベントハンドラを追加することができます。イベントハンドラは、クリック、ホバー、フォーム入力へのフォーカスなどのユーザインタラクションに応答して起動する独自の関数です。
<button>
のような組み込みコンポーネントは onClick
のような組み込みのブラウザイベントのみをサポートします。しかし、独自のコンポーネントを作成し、そのイベントハンドラ props に任意のアプリケーション固有の名前を付けることもできます。
export default function App() { return ( <Toolbar onPlayMovie={() => alert('Playing!')} onUploadImage={() => alert('Uploading!')} /> ); } function Toolbar({ onPlayMovie, onUploadImage }) { return ( <div> <Button onClick={onPlayMovie}> Play Movie </Button> <Button onClick={onUploadImage}> Upload Image </Button> </div> ); } function Button({ onClick, children }) { return ( <button onClick={onClick}> {children} </button> ); }
state:コンポーネントのメモリ
コンポーネントはインタラクションの結果として画面に表示されるものを変更する必要があることがよくあります。フォームに入力すると入力フィールドが更新され、イメージカルーセルで「次へ」をクリックすると表示されるイメージが変更され、「購入」をクリックするとショッピングカートに商品が入ります。コンポーネントは、現在の入力値、現在のイメージ、ショッピングカートを「記憶」する必要があるのです。React では、このようなコンポーネント固有の記憶を state と呼びます。
useState
フックを使用すると、コンポーネントに state を追加することができます。フックとはコンポーネントに React の機能を使用させるための特別な関数です(state はその機能の 1 つです)。useState
フックで state 変数を宣言できます。これは初期 state を受け取り、現在の state と、それを更新するための state セッタ関数のペアを返します。
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
以下は、イメージギャラリがクリックされた時に state を更新する方法です:
import { useState } from 'react'; import { sculptureList } from './data.js'; export default function Gallery() { const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); const hasNext = index < sculptureList.length - 1; function handleNextClick() { if (hasNext) { setIndex(index + 1); } else { setIndex(0); } } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; return ( <> <button onClick={handleNextClick}> Next </button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <button onClick={handleMoreClick}> {showMore ? 'Hide' : 'Show'} details </button> {showMore && <p>{sculpture.description}</p>} <img src={sculpture.url} alt={sculpture.alt} /> </> ); }
レンダーとコミット
コンポーネントは、画面で表示される前に React によってレンダーされる必要があります。このプロセスのステップを理解することで、コードの実行方法について考え、その動作を説明することができるようになります。
コンポーネントは厨房で材料から美味しい料理を作る料理人だと想像してください。このシナリオでは React はお客様のリクエストを受け付け、注文された料理を運ぶウェイターです。注文を受けて UI 要素を「配膳」するプロセスには、次の 3 つのステップが存在します:
- レンダーのトリガ(お客様の注文を厨房に届ける)
- コンポーネントのレンダー(厨房で注文された料理を準備する)
- DOM へのコミット(テーブルに注文を置く)
Illustrated by Rachel Lee Nabors
state はスナップショットである
通常の JavaScript の変数とは異なり、React の state はスナップショットのような動作をします。これを設定しても既存の state 変数は変更されず、代わりに再レンダーがトリガされます。このような動作に最初は驚くかもしれませんね。
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
React はこのような仕組みになっているので、微妙なバグを回避することができます。ここに小さなチャットアプリがあります。まず「送信」を押して、次に受信者を「ボブ」に変更したら何が起こるか、推測してみてください。5 秒後の alert
には誰の名前が表示されるでしょうか?
import { useState } from 'react'; export default function Form() { const [to, setTo] = useState('Alice'); const [message, setMessage] = useState('Hello'); function handleSubmit(e) { e.preventDefault(); setTimeout(() => { alert(`You said ${message} to ${to}`); }, 5000); } return ( <form onSubmit={handleSubmit}> <label> To:{' '} <select value={to} onChange={e => setTo(e.target.value)}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> </select> </label> <textarea placeholder="Message" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); }
Ready to learn this topic?
イベントハンドラ内で state が「固定」され、変化していないように見える理由を学ぶには state はスナップショットである を読んでみましょう。
Read More一連の state の更新をキューに入れる
このコンポーネントにはバグがあります:“+3” をクリックしても 1 しかスコアが増えません。
import { useState } from 'react'; export default function Counter() { const [score, setScore] = useState(0); function increment() { setScore(score + 1); } return ( <> <button onClick={() => increment()}>+1</button> <button onClick={() => { increment(); increment(); increment(); }}>+3</button> <h1>Score: {score}</h1> </> ) }
state はスナップショットである で、なぜこのようなことが起こってしまうのかを説明しています。state を設定すると、新しい再レンダーが要求されますが、すでに実行されているコード内の state は変更されません。そのため setScore(score + 1)
を呼び出した直後は score
が 0
であり続けます。
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
state を設定する際に更新用関数を渡すことでこれを修正することができます。setScore(score + 1)
を setScore(s => s + 1)
に置き換えることで、“+3” ボタンが修正されることに注目してください。これは、複数の state の更新をキューに入れる必要がある場合に便利です。
import { useState } from 'react'; export default function Counter() { const [score, setScore] = useState(0); function increment() { setScore(s => s + 1); } return ( <> <button onClick={() => increment()}>+1</button> <button onClick={() => { increment(); increment(); increment(); }}>+3</button> <h1>Score: {score}</h1> </> ) }
state 内のオブジェクトを更新する方法
State はオブジェクトを含むあらゆる種類の JavaScript の値を保持することができます。しかし、React の state 内で保持するオブジェクトや配列を直接変更してはいけません。その代わり、オブジェクトや配列を更新したい場合、既存のもののコピーを作るなどして新しい値を作成し、その新しい値を使って state を変更する必要があります。
通常、変更したいオブジェクトや配列をコピーするには ...
というスプレッド構文を使用します。例えば、ネストされたオブジェクトを更新する場合、次のようになります:
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
コード内のオブジェクトのコピーが面倒になったら Immer のようなライブラリを使うと、コードの繰り返しを減らすことができます:
import { useImmer } from 'use-immer'; export default function Form() { const [person, updatePerson] = useImmer({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { updatePerson(draft => { draft.name = e.target.value; }); } function handleTitleChange(e) { updatePerson(draft => { draft.artwork.title = e.target.value; }); } function handleCityChange(e) { updatePerson(draft => { draft.artwork.city = e.target.value; }); } function handleImageChange(e) { updatePerson(draft => { draft.artwork.image = e.target.value; }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
state 内の配列を更新する方法
配列もまた、state 内で保持できるミュータブル(mutable; 書き換え可能)な JavaScript オブジェクトの一種ですが、読み取り専用として扱うべきものです。オブジェクトと同様に state に保存された配列を更新したい場合、新しいものを作成し(または既存のもののコピーを作成し)state が新しい配列を使用するように設定する必要があります:
import { useState } from 'react'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [list, setList] = useState( initialList ); function handleToggle(artworkId, nextSeen) { setList(list.map(artwork => { if (artwork.id === artworkId) { return { ...artwork, seen: nextSeen }; } else { return artwork; } })); } return ( <> <h1>Art Bucket List</h1> <h2>My list of art to see:</h2> <ItemList artworks={list} onToggle={handleToggle} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }
コード内の配列のコピーが面倒になったら Immer のようなライブラリを使うと、コードの繰り返しを減らすことができます:
import { useState } from 'react'; import { useImmer } from 'use-immer'; let nextId = 3; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [list, updateList] = useImmer(initialList); function handleToggle(artworkId, nextSeen) { updateList(draft => { const artwork = draft.find(a => a.id === artworkId ); artwork.seen = nextSeen; }); } return ( <> <h1>Art Bucket List</h1> <h2>My list of art to see:</h2> <ItemList artworks={list} onToggle={handleToggle} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }