Reactを学習していて親子関係を持つコンポーネントが分からなかったので整理します。
なぜコンポーネントを親-子に分割するのか
- コンポーネントの肥大化を避ける
- データ(stateやprops)のスコープを制限するため
コンポーネントが肥大化すると可読性が下がります。また、複数のデータを1つのコンポーネントで管理すると保守性が低下します。
このような弊害を回避するためにコンポーネントを分割するものと思われます。
親子コンポーネントの基本的な実装方針
- アイテム一覧やアイテム一覧の更新処理は親コンポーネントで管理する
- 子コンポーネントは不要な情報を持たない
- データの表示のみを責務とする子コンポーネントは状態を持たない
以下のサンプルコードを例にして順番に見ていきましょう。
(フォームに文字列を入力して投稿ボタンを押すと投稿一覧が画面に表示される簡単なアプリケーションです。)
import { useState } from 'react';
// 親コンポーネント
function App() {
// 投稿一覧を管理するatate
const [itemList, setItemList] = useState([]);
// ボタンがクリックされたときに投稿一覧を更新する
const handleButtonClick = val => {
setItemList([...itemList, val]);
}
return (
<>
{/* 入力フォームコンポーネントにpropsとしてイベントハンドラを渡す */}
<Input onClick={handleButtonClick} />
{/* Itemコンポーネントに入力値を渡す*/}
{itemList.map(item => (
<Item value={ item } />
))}
</>
);
}
// 入力フォームとボタンを表示するコンポーネント
function Input({ onClick }) {
// 入力値
const [val, setVal] = useState('');
// 入力値の変更を検知した時にstateを更新する
const handleValChange = e => {
setVal(e.target.value);
}
// ボタンがクリックされた時に親コンポーネントから渡されたイベントハンドラを実行する
// 子コンポーネントはその処理内容を知らない
const handleClick = () => {
onClick(val)
}
return (
<>
<input type="text" value={val} onChange={handleValChange} />
<button onClick={handleClick}>Add</button>
</>
)
}
// 投稿を表示するコンポーネント
function Item({value}) {
return <p>{value}</p>;
}
export default App;
1. アイテム一覧やアイテム一覧の更新処理は親コンポーネントで管理する
function App() {
// 投稿一覧を管理するatate
const [itemList, setItemList] = useState([]);
// ボタンがクリックされたときに投稿一覧を更新する
const handleButtonClick = val => {
setItemList([...itemList, val]);
}
// 以下省略
Appコンポーネントで投稿一覧をstateとして管理しています。入力値の管理を責務とするItemコンポーネントや投稿内容1つを表示することを責務とするItemコンポーネントで投稿一覧を管理することは不自然であるためです。(画面が複雑になった場合、投稿一覧の管理を責務とするItemListコンポーネントに切り出す方がよいかもしれません)
itemListを管理するのはAppコンポーネントの責務なので、投稿追加処理を管理するのもこのコンポーネントの責務とします。
Appコンポーネントが管理しているitemList stateとhandleButtonClick関数をpropsとして子コンポーネント(ItemとInput)に渡しています。
return (
<>
{/* 入力フォームコンポーネントにpropsとしてイベントハンドラを渡す */}
<Input onClick={handleButtonClick} />
{/* Itemコンポーネントに入力値を渡す*/}
{itemList.map(item => (
<Item value={ item } />
))}
</>
);
2. 子コンポーネントは不要な情報を持たない
Inputコンポーネントを例にとって考えます。
// 入力フォームとボタンを表示するコンポーネント
function Input({ onClick }) {
// 入力値
const [val, setVal] = useState('');
// 入力値の変更を検知した時にstateを更新する
const handleValChange = e => {
setVal(e.target.value);
}
// ボタンがクリックされた時に親コンポーネントから渡されたイベントハンドラを実行する
// 子コンポーネントはその処理内容を知らない
const handleClick = () => {
onClick(val)
}
return (
<>
<input type="text" value={val} onChange={handleValChange} />
<button onClick={handleClick}>Add</button>
</>
)
}
このコンポーネントはフォームの入力値をstateとして管理しています。
input要素の値が変更された場合、その変更を検知してstateの値を更新します。
Inputコンポーネントは入力値と入力値が変化した場合の処理を知っています。
その一方で、button要素がクリックされた場合の処理については知りません。ただ親コンポーネントからpropsとして渡されたonClick関数を呼び出すだけです。
「ボタンがクリックされたら入力された文字列をそのままitemListに追加する」という処理をInputコンポーネントは知る必要はありません。
投稿一覧の更新処理はInputコンポーネントの責任範囲外であるためです。(親コンポーネントのstateを子コンポーネントから直接操作できないという技術的な制約もあります。)
3. データの表示のみを責務とする子コンポーネントは状態を持たない
// (AppコンポーネントとInputコンポーネントは省略)
// 投稿を表示するコンポーネント
function Item({value}) {
return <p>{value}</p>;
}
Itemコンポーネントは入力値をpropsとして親コンポーネントから受け取って表示しています。それ以外は何もしていません。Inputコンポーネントと違い、stateを持ちません。
親子コンポーネントからpropsを受け取って表示するだけなので修正が容易ですしバグが混入するリスクを抑えることもできます。