はじめに
本記事では、Reactにおけるprops drilling(バケツリレー)に焦点をあて、深くなりすぎたコンポーネントに対して状態・変数を共有する方法について考えていきます。
「必要なコンポーネントに必要な値を渡す」という直観的なprops drilling自体は問題ではないですが、コンポーネントのネストが深くなりすぎた場合は以下の問題点が生じます。
- リファクタリング時に、バケツリレーに関わる全てのコンポーネントの修正が必要
- 状態やロジックを上層コンポーネントの一箇所で管理している場合、下層コンポーネントまで状態やロジックをバケツリレーする必要がある
- 中間のコンポーネントには、関係のないpropsが渡ってしまう
上記を解決するために、Reactには、Context APIやReduxで状態を共有する方法が存在します。
今回の話は、Context APIやライブラリを使う前にやるべきprops drilling問題の解消法に触れていきます。
問題提起
以下のコードでは、
上層コンポーネントKanban
の状態と更新関数
を
下層コンポーネントTask
にprops drillingで渡しています。
問題点
親→子→孫のようにネストを深くしてprops drillingするコンポーネント設計では、こんな問題が生じます。
- 中間層コンポーネント
TaskList
では不必要なpropsが渡っている - 変数が増えた場合、中間層・下層のコンポーネントに対してpropsのメンテナンスが困難になる
コード
// 上層コンポーネント
export const Kanban: React.FC = () => {
const [tasks, setTasks] = useState<Task[]>([
{ id: "0", name: "foo", isDone: true },
{ id: "1", name: "bar", isDone: false },
{ id: "2", name: "baz", isDone: false }
])
const switchIsDone = (taskId: string) => {
setTasks(
tasks.map((task) => {
if (task.id === taskId) {
return {
...task,
isDone: !task.isDone
}
}
return task
})
)
}
return (
<div>
<h1>Header</h1>
{/* ここでprops drillingが生じる */}
<TaskList tasks={tasks} handleSwitchIsDone={switchIsDone} />
<div>Footer</div>
</div>
)
}
// 中間層コンポーネント
export const TaskList: React.FC<TaskListProps> = ({
tasks,
handleSwitchIsDone
}) => {
return (
<div>
{tasks.map((task) => (
// ここでprops drillingが生じる
<Task key={task.id} task={task} onClick={handleSwitchIsDone} />
))}
</div>
)
}
// 下層コンポーネント
export const Task: React.FC<TaskProps> = ({ task, onClick }) => {
return (
<div>
<input
type="checkbox"
checked={task.isDone}
onClick={() => onClick(task.id)}
/>
{task.name}
</div>
)
}
解決法: Compositionで親と子のコンポーネント間の距離を縮める
結論
上記の問題を解決するシンプルな方法は、
Compositionを利用したコンポーネント設計にすること
です。
Compositionって?
ReactにおけるCompositionとは、
子コンポーネントの要素を親に委譲すること
です。
先ほどのコードを改善するならば、こんなイメージになります。
子コンポーネントでレンダリングするべき要素をprops.children
にすることで、
親コンポーネントから子コンポーネントの要素を指定できます。
これを応用すると、
親の状態やロジックに依存する子要素にダイレクトにアクセスすることができるようになります。
Compositionの考え方
- 下層コンポーネントの要素を
props.children
にして上層からのアクセス性を確保する - 下層コンポーネントを汎用的な箱として使うことで、上層から柔軟に使えるコンポーネントにする
改善したコード
中間層と下層のコンポーネントをCompositionを利用して以下のように修正しました。
export const TaskList: React.FC<TaskListProps> = ({ children }) => {
return <div>{children}</div>
}
export const Task: React.FC<TaskProps> = ({ children }) => {
return <div>{children}</div>
}
Compositionを利用することで、
props drillingせずに上層の状態やロジックに直接アクセスできるようになります。
export const Kanban: React.FC = () => {
const [tasks, setTasks] = useState<Task[]>([
{ id: "0", name: "foo", isDone: true },
{ id: "1", name: "bar", isDone: false },
{ id: "2", name: "baz", isDone: false }
])
const switchIsDone = (taskId: string) => {
setTasks(
tasks.map((task) => {
if (task.id === taskId) {
return {
...task,
isDone: !task.isDone
}
}
return task
})
)
}
return (
<div>
<h1>Tasks</h1>
<TaskList>
{/* TaskListのchildren */}
{tasks.map((task) => (
<Task key={task.id}> // ← 直接アクセスできる
{/* Taskのchildren */}
<input
type="checkbox"
checked={task.isDone} // ← 直接アクセスできる
onClick={() => switchIsDone(task.id)} // ← 直接アクセスできる
/>
{task.name} // ← 直接アクセスできる
</Task>
))}
</TaskList>
<div>Footer</div>
</div>
)
}
まとめ
本記事では、props drilling(バケツリレー)に焦点をあてて、ネストが深すぎた場合の解決方法を紹介しました。
ReactのCompositionを利用することで、以下のメリットを得ることができます。
- props drillingの問題点を解消する
- 上層の状態やロジックへのアクセス性を確保する
- 柔軟なコンポーネント設計を実現する
同じような問題に遭遇した場合はぜひ参考にしてみてください。
参考
Reactのパターン(Composition, Compound Components)を使ってリファクタリング
Reactのprops drilling(バケツリレー)とhooksに我々はどう立ち向かっていけばよいのか
【React】Context を使う前に #2 コンポジション (ReactNode 型の Props) を使え
Reactの設計やパフォーマンス改善でお困りですか?お気軽にご相談ください。