インタラクティブ機能の追加
画面上のいくつかの要素は、ユーザー入力に応じて更新されます。たとえば、画像ギャラリーをクリックすると、アクティブな画像が切り替わります。React では、時間とともに変化するデータは状態と呼ばれます。任意のコンポーネントに状態を追加し、必要に応じて更新できます。この章では、インタラクションを処理し、状態を更新し、時間とともに異なる出力を表示するコンポーネントの書き方を学びます。
この章の内容
イベントへの対応
React では、JSX にイベントハンドラを追加できます。イベントハンドラは、クリック、ホバー、フォーム入力へのフォーカスなど、ユーザーインタラクションに応じてトリガーされる独自の関数です。
<button>
などの組み込みコンポーネントは、onClick
などの組み込みブラウザイベントしかサポートしていません。ただし、独自のコンポーネントを作成し、そのイベントハンドラプロップに任意のアプリケーション固有の名前を付けることもできます。
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> ); }
状態:コンポーネントのメモリ
コンポーネントは、インタラクションの結果として画面上の内容を変更する必要があることがよくあります。フォームに入力すると入力フィールドが更新され、「次へ」をクリックすると表示される画像が変更され、「購入」をクリックすると商品がショッピングカートに追加されます。コンポーネントは、現在の入力値、現在の画像、ショッピングカートなど、物事を「記憶」する必要があります。React では、この種のコンポーネント固有のメモリは状態と呼ばれます。
useState
フックを使用して、コンポーネントに状態を追加できます。フックは、コンポーネントが React の機能(状態はその 1 つ)を使用できるようにする特別な関数です。useState
フックを使用すると、状態変数を宣言できます。初期状態を受け取り、2 つの値のペア(現在の状態と、それを更新できる状態設定関数)を返します。
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
クリック時に画像ギャラリーが状態を使用および更新する方法を次に示します。
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 へ(注文をテーブルに置く)
トリガー レンダリング コミット
スナップショットとしての状態
通常のJavaScript変数とは異なり、Reactの状態はスナップショットのように動作します。状態を設定しても、既に存在する状態変数は変更されず、代わりに再レンダリングがトリガーされます。これは最初は驚くかもしれません!
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
この動作により、微妙なバグを回避できます。小さなチャットアプリを例に挙げてみましょう。「送信」ボタンを先に押し、その後受信者をBobに変更した場合に何が起こるか考えてみてください。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> ); }
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> </> ) }
スナップショットとしての状態では、これがなぜ起こるのかを説明しています。状態を設定すると新しい再レンダリングが要求されますが、既に実行中のコード内の状態は変更されません。score
は、setScore(score + 1)
を呼び出した後も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
状態を設定する際に、更新関数を渡すことでこれを修正できます。setScore(score + 1)
をsetScore(s => s + 1)
に置き換えると「+3」ボタンが修正されることに注目してください。これにより、複数の状態更新をキューイングできます。
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> </> ) }
状態内のオブジェクトの更新
状態には、オブジェクトを含むあらゆる種類のJavaScript値を格納できます。しかし、Reactの状態に直接保持しているオブジェクトや配列を変更しないでください。オブジェクトや配列を更新する場合は、新しいオブジェクト(または既存オブジェクトのコピー)を作成し、そのコピーを使用して状態を更新する必要があります。
通常、変更したいオブジェクトや配列をコピーするには、...
スプレッド構文を使用します。たとえば、ネストされたオブジェクトの更新は次のようになります。
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のようなライブラリを使用して、反復的なコードを削減できます。
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
状態内の配列の更新
配列は、状態に格納できる別の種類の変更可能なJavaScriptオブジェクトであり、読み取り専用として扱う必要があります。オブジェクトと同様に、状態に格納されている配列を更新する場合は、新しい配列(または既存配列のコピー)を作成し、新しい配列を使用して状態を設定する必要があります。
import { useState } from 'react'; 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のようなライブラリを使用して、反復的なコードを削減できます。
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }