アプリケーションが大きくなるにつれて、状態の構成方法とコンポーネント間のデータフローについて、より意図的に考えることが役立ちます。冗長な状態や重複した状態は、バグの一般的な原因となります。この章では、状態を適切に構造化する方法、状態更新ロジックを保守しやすい状態に保つ方法、および離れたコンポーネント間で状態を共有する方法について学習します。
この章の内容
状態を用いた入力への反応
React では、コードから UI を直接変更することはありません。たとえば、「ボタンを無効にする」、「ボタンを有効にする」、「成功メッセージを表示する」などのコマンドは記述しません。代わりに、コンポーネントのさまざまな視覚状態(「初期状態」、「入力状態」、「成功状態」)に表示したい UI を記述し、ユーザー入力に応じて状態変更をトリガーします。これは、デザイナーが UI について考える方法に似ています。
これは、React を使用して構築されたクイズフォームです。送信ボタンを有効または無効にするか、成功メッセージを表示するかを決定するために、`status` 状態変数を使用する方法に注目してください。
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
状態構造の選択
状態を適切に構造化することで、コンポーネントの変更とデバッグが容易になり、バグの発生源となるのを防ぐことができます。最も重要な原則は、状態に冗長な情報や重複した情報を含めないことです。不要な状態があると、更新を忘れてバグが発生しやすくなります。
たとえば、このフォームには**冗長な** `fullName` 状態変数があります。
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
コンポーネントのレンダリング中に `fullName` を計算することで、それを削除してコードを簡略化できます。
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
これは小さな変更のように思えるかもしれませんが、React アプリの多くのバグはこの方法で修正されます。
コンポーネント間での状態の共有
場合によっては、2 つのコンポーネントの状態を常に一緒に変更したい場合があります。そのためには、両方のコンポーネントから状態を削除し、最も近い共通の親に移動してから、props 経由でそれらに渡します。これは「状態のリフトアップ」として知られており、React コードを書く際によく行うことの 1 つです。
この例では、一度に 1 つのパネルのみをアクティブにする必要があります。これを達成するために、アクティブ状態を個々のパネル内に保持する代わりに、親コンポーネントが状態を保持し、その子コンポーネントのプロパティを指定します。
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city. </Panel> <Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Show </button> )} </section> ); }
状態の保存とリセット
コンポーネントを再レンダリングすると、React はツリーのどの部分を保持(および更新)し、どの部分を破棄またはゼロから再作成するかを決定する必要があります。ほとんどの場合、React の自動動作で十分です。デフォルトでは、React は以前にレンダリングされたコンポーネントツリーと「一致する」ツリーの部分を保持します。
しかし、時にはこれはあなたが望むものではありません。このチャットアプリでは、メッセージを入力してから受信者を切り替えても、入力がリセットされません。これにより、ユーザーが誤って間違った人にメッセージを送信してしまう可能性があります。
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
React では、デフォルトの動作をオーバーライドし、<Chat key={email} />
のように、異なる key
を渡すことで、コンポーネントのステートを*強制的に*リセットできます。これは、受信者が異なる場合、新しいデータ(および入力などのUI)でゼロから再作成する必要がある*異なる* Chat
コンポーネントと見なす必要があることを React に指示します。これで、同じコンポーネントをレンダリングしても、受信者を切り替えると入力フィールドがリセットされます。
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.email} contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
状態ロジックをレデューサーに抽出する (リンクアイコンSVG)
多くのイベントハンドラーにまたがって多くの状態更新を持つコンポーネントは、圧倒される可能性があります。このような場合、「レデューサー」と呼ばれる単一の関数で、コンポーネント外のすべての状態更新ロジックを統合できます。イベントハンドラーは、ユーザーの「アクション」のみを指定するため、簡潔になります。ファイルの下部にあるレデューサー関数は、各アクションに応じて状態を更新する方法を指定します。
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Visit Kafka Museum', done: true }, { id: 1, text: 'Watch a puppet show', done: false }, { id: 2, text: 'Lennon Wall pic', done: false } ];
コンテキストを使用してデータを深く渡す (リンクアイコンSVG)
通常、props を介して親コンポーネントから子コンポーネントに情報を渡します。しかし、多くのコンポーネントを介して props を渡す必要がある場合、または多くのコンポーネントが同じ情報を必要とする場合、props の受け渡しは不便になる可能性があります。コンテキストを使用すると、親コンポーネントは、props を明示的に渡さなくても、ツリー内のその下の任意のコンポーネント(どれだけ深くても)でいくつかの情報を利用できるようにすることができます。
ここで、Heading
コンポーネントは、最も近い Section
にレベルを「尋ねる」ことで、見出しレベルを決定します。各 Section
は、親の Section
に尋ねて 1 を加えることで、独自のレベルを追跡します。すべての Section
は、props を渡さずに、その下のすべてのコンポーネントに情報を提供します。これはコンテキストを介して行われます。
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
レデューサーとコンテキストによるスケールアップ (リンクアイコンSVG)
レデューサーを使用すると、コンポーネントの状態更新ロジックを統合できます。コンテキストを使用すると、他のコンポーネントに情報を深く渡すことができます。レデューサーとコンテキストを組み合わせて、複雑な画面の状態を管理できます。
このアプローチでは、複雑な状態を持つ親コンポーネントは、レデューサーを使用してそれを管理します。ツリー内の任意の場所にある他のコンポーネントは、コンテキストを介してその状態を読み取ることができます。また、アクションをディスパッチしてその状態を更新することもできます。
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
次は? (リンクアイコンSVG)
ステートによる入力への反応 にアクセスして、この章をページごとに読み始めてください。
または、これらのトピックに既に精通している場合は、エスケープハッチ について読んでみませんか?