⛏️

ReactでContext APIを使う前にやるべきprops drilling問題の解消法

2024/03/24に公開

はじめに

本記事では、Reactにおけるprops drilling(バケツリレー)に焦点をあて、深くなりすぎたコンポーネントに対して状態・変数を共有する方法について考えていきます。

「必要なコンポーネントに必要な値を渡す」という直観的なprops drilling自体は問題ではないですが、コンポーネントのネストが深くなりすぎた場合は以下の問題点が生じます。

  • リファクタリング時に、バケツリレーに関わる全てのコンポーネントの修正が必要
  • 状態やロジックを上層コンポーネントの一箇所で管理している場合、下層コンポーネントまで状態やロジックをバケツリレーする必要がある
  • 中間のコンポーネントには、関係のないpropsが渡ってしまう

上記を解決するために、Reactには、Context APIやReduxで状態を共有する方法が存在します。
今回の話は、Context APIやライブラリを使う前にやるべきprops drilling問題の解消法に触れていきます。

問題提起

以下のコードでは、
上層コンポーネントKanbanの状態と更新関数

下層コンポーネントTaskにprops drillingで渡しています。

問題点

親→子→孫のようにネストを深くしてprops drillingするコンポーネント設計では、こんな問題が生じます。

  • 中間層コンポーネントTaskListでは不必要なpropsが渡っている
  • 変数が増えた場合、中間層・下層のコンポーネントに対してpropsのメンテナンスが困難になる

コード

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>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を利用して以下のように修正しました。

TaskList.tsx
export const TaskList: React.FC<TaskListProps> = ({ children }) => {
  return <div>{children}</div>
}
Task.tsx
export const Task: React.FC<TaskProps> = ({ children }) => {
  return <div>{children}</div>
}

Compositionを利用することで、
props drillingせずに上層の状態やロジックに直接アクセスできるようになります。

Kanban.tsx
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の問題点を解消する
  • 上層の状態やロジックへのアクセス性を確保する
  • 柔軟なコンポーネント設計を実現する

同じような問題に遭遇した場合はぜひ参考にしてみてください。

参考

https://qiita.com/myktsk/items/5f4d443cd7e10686f189
https://www.asobou.co.jp/blog/web/props-drilling
https://qiita.com/honey32/items/4d04e454550fb1ed922c#ok-コンポジションパターン

Discussion