7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React】propsのバケツリレーを撲滅する。

Last updated at Posted at 2022-07-24

こんにちは、Webフロントをしているものです。
今回はReactを開発する上で必須ともいえるCompositionという技をご紹介したいなと思います。

Reactでただ開発していると必ずバケツリレーしまくってしまうという問題にぶち当たると思います。

やはりバケツリレーが増えると、
値が追いにくくなったりして開発体験がめちゃくちゃ悪くなってしますよね。

そんな悩みを解決するのがCompositionというものです。

簡単にいうとCompositionとは、
コンポーネント自体をpropsとして渡すことでバケツリレーを減らすことができるものです。

普通に作ってみる

まずは例として、以下のようなTodoアプリを作りました。

スクリーンショット 2022-05-30 1.32.14.png

コードとしては以下の通りです。

import { useState } from "react";

type Todo = {
  id: number;
  text: string;
  isDone: boolean;
};

type LeftElemProps = {
  todo: Todo;
  doneTodo: (id: number) => void;
};

const LeftElem = (props: LeftElemProps) => {
  const { doneTodo, todo } = props;

  return (
    <>
      <input
        type="checkbox"
        checked={todo.isDone}
        onChange={() => {
          return doneTodo(todo.id);
        }}
      />
      <label className="ml-2">{todo.text}</label>
    </>
  );
};

type RightElemProps = {
  id: number;
  deleteTodo: (id: number) => void;
};

const RightElem = (props: RightElemProps) => {
  const { deleteTodo, id } = props;
  return (
    <button
      className="py-1 px-3 mr-0 ml-auto text-white bg-red-500 rounded-lg outline-none"
      onClick={() => {
        return deleteTodo(id);
      }}
    >
      削除
    </button>
  );
};

type TodoListItemProps = {
  todo: Todo;
  doneTodo: (id: number) => void;
  deleteTodo: (id: number) => void;
};

const TodoListItem = (props: TodoListItemProps) => {
  const { deleteTodo, doneTodo, todo } = props;

  return (
    <li className="flex items-center py-4 px-6 mb-2 rounded-md border">
      {<LeftElem doneTodo={doneTodo} todo={todo} />}
      {<RightElem deleteTodo={deleteTodo} id={todo.id} />}
    </li>
  );
};

const TodoPage = () => {
  const [todoList, setTodoList] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState("");

  const addTodo = () => {
    const lastTodoId = Math.max(
      ...[
        ...todoList.map((todo) => {
          return todo.id;
        }),
        0,
      ]
    );

    const newTodo: Todo = {
      id: lastTodoId + 1,
      text: inputText,
      isDone: false,
    };
    setTodoList([...todoList, newTodo]);
    setInputText("");
  };

  const doneTodo = (id: number) => {
    setTodoList((prev) => {
      return prev.map((todo) => {
        if (todo.id === id) {
          return {
            ...todo,
            isDone: !todo.isDone,
          };
        }
        return todo;
      });
    });
  };

  const deleteTodo = (id: number) => {
    setTodoList(
      todoList.filter((todo) => {
        return todo.id !== id;
      })
    );
  };

  return (
    <div className="mx-auto mt-[100px] w-96 text-center">
      <h2 className="mb-4 text-xl">Todo</h2>
      <div>
        <input
          type="text"
          className="mb-4 h-8 rounded-md border-gray-200"
          value={inputText}
          onChange={(e) => {
            return setInputText(e.target.value);
          }}
        />
        <button
          className="py-2 px-4 ml-2 text-white bg-blue-500 rounded-lg outline-none"
          onClick={addTodo}
        >
          追加する
        </button>
      </div>
      <ul>
        {todoList.map((todo) => {
          return (
            <TodoListItem
              key={todo.id}
              todo={todo}
              doneTodo={doneTodo}
              deleteTodo={deleteTodo}
            />
          );
        })}
      </ul>
    </div>
  );
};

export default TodoPage;

上記をコンポーネントの図にすると以下の通りです。

スクリーンショット 2022-05-30 1.59.28.png

上から、

TodoPage:ページ全体を表示する。受け取るprops無し

TodoListItem:todoのリストを表示する。todo, donetodo, deleteTodo

LeftElem:リストにおける左部分。todo, doneTodo

RightElem:リストにおける右部分。id, deleteTodo

のようになります。

簡単にですが、
上記のような構成でTodoアプリを実装しました。

ここで気づいていただきたいのは、
TodoListItemの責務が、
todoのリストを表示することです。

なので、
propsとしてtodoやdonetodo、deleteTodoを受け取るのは余分なのです。

実際にTodoListItemの中では、
受け取ったpropsのうちのひとつたりとも使用していません。
これはまさに無駄なバケツリレーをしてしまっているということでしょう。

これらを改善するにはどうするべきか。
そう、Compositionの出番なのです。

先ほどのコードをCompositionで書き換える

先ほどのコードをCompositionを駆使して書き換えてみると、
コードは以下のようになりました。

import { useState } from "react";

type Todo = {
  id: number;
  text: string;
  isDone: boolean;
};

type LeftElemProps = {
  todo: Todo;
  doneTodo: (id: number) => void;
};

const LeftElem = (props: LeftElemProps) => {
  const { doneTodo, todo } = props;

  return (
    <>
      <input
        type="checkbox"
        checked={todo.isDone}
        onChange={() => {
          return doneTodo(todo.id);
        }}
      />
      <label className="ml-2">{todo.text}</label>
    </>
  );
};

type RightElemProps = {
  id: number;
  deleteTodo: (id: number) => void;
};

const RightElem = (props: RightElemProps) => {
  const { deleteTodo, id } = props;
  return (
    <button
      className="py-1 px-3 mr-0 ml-auto text-white bg-red-500 rounded-lg outline-none"
      onClick={() => {
        return deleteTodo(id);
      }}
    >
      削除
    </button>
  );
};

type TodoListItemProps = {
  LeftElem: React.ReactNode;
  RightElem: React.ReactNode;
};

const TodoListItem = (props: TodoListItemProps) => {
  const { LeftElem, RightElem } = props;

  return (
    <li className="flex items-center py-4 px-6 mb-2 rounded-md border">
      {LeftElem}
      {RightElem}
    </li>
  );
};

const TodoPage = () => {
  const [todoList, setTodoList] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState("");

  const addTodo = () => {
    const lastTodoId = Math.max(
      ...[
        ...todoList.map((todo) => {
          return todo.id;
        }),
        0,
      ]
    );
    const newTodo: Todo = {
      id: lastTodoId + 1,
      text: inputText,
      isDone: false,
    };
    setTodoList([...todoList, newTodo]);
    setInputText("");
  };

  const doneTodo = (id: number) => {
    setTodoList((prev) => {
      return prev.map((todo) => {
        if (todo.id === id) {
          return {
            ...todo,
            isDone: !todo.isDone,
          };
        }
        return todo;
      });
    });
  };

  const deleteTodo = (id: number) => {
    setTodoList(
      todoList.filter((todo) => {
        return todo.id !== id;
      })
    );
  };

  return (
    <div className="mx-auto mt-[100px] w-96 text-center">
      <h2 className="mb-4 text-xl">Todo</h2>
      <div>
        <input
          type="text"
          className="mb-4 h-8 rounded-md border-gray-200"
          value={inputText}
          onChange={(e) => {
            return setInputText(e.target.value);
          }}
        />
        <button
          className="py-2 px-4 ml-2 text-white bg-blue-500 rounded-lg outline-none"
          onClick={addTodo}
        >
          追加する
        </button>
      </div>
      <ul>
        {todoList.map((todo) => {
          return (
            <TodoListItem
              key={todo.id}
              LeftElem={<LeftElem todo={todo} doneTodo={doneTodo} />}
              RightElem={<RightElem deleteTodo={deleteTodo} id={todo.id} />}
            />
          );
        })}
      </ul>
    </div>
  );
};

export default TodoPage;

注目して欲しいのは、

type TodoListItemProps = {
  LeftElem: React.ReactNode;
  RightElem: React.ReactNode;
};

const TodoListItem = (props: TodoListItemProps) => {
  const { LeftElem, RightElem } = props;

  return (
    <li className="flex items-center py-4 px-6 mb-2 rounded-md border">
      {LeftElem}
      {RightElem}
    </li>
  );
};

の部分です。

こちらは、
propsにtodoやdonetodo、deleteTodoを受け取っていたのを、
LeftElem、RightElemを受け取っています。

つまり、
以下の画像のようになっています。

スクリーンショット 2022-05-30 2.14.45.png

そう、
このような設計にすれば、
なんと全くもってpropsのバケツリレーをすることがないのです。

つまり、
本来のTodoListItemの責務である、
todoのリストを表示することを従順に守ることができ、
余分に使わないpropsを受け取ることがなくなるんです。

なので、
値は全て最上部のコンポーネントから流し込まれることとなり、
値が追いにくい!という状況は打破できるのです。

しかもこうすれば、
propsを大量に受け渡すことによるパフォーマンスの低下も避けられます。

(Reactはpropsに変化がないか見比べるのにリソースを使うため)

これで、
いい感じにTodoアプリを実装できました!
ありがとうございます!

最後に

今回紹介したCompositionはReactの公式ドキュメントのMain Conceptsの方に載ってます。

TypeScriptを使うと、
ある程度は値は追いやすくなると思います。

しかしそれでも規模が大きくなってくると、
コンポーネント自体の責務が曖昧になり、
開発効率を低下させてしまうことがあります。

そういった中で、
Compositionのようなものを使えば、
コンポーネントごとの責務、役割を明確にできる。

それによって開発効率を上げることができ、
チームのバリューが増すと思います。

なので、
常にそういったアンテナを貼り続けて
チーム全体としての価値を上げていけたらなと。

以上、ありがとうございました。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?