こんにちは、Webフロントをしているものです。
今回はReactを開発する上で必須ともいえるCompositionという技をご紹介したいなと思います。
Reactでただ開発していると必ずバケツリレーしまくってしまうという問題にぶち当たると思います。
やはりバケツリレーが増えると、
値が追いにくくなったりして開発体験がめちゃくちゃ悪くなってしますよね。
そんな悩みを解決するのがCompositionというものです。
簡単にいうとCompositionとは、
コンポーネント自体をpropsとして渡すことでバケツリレーを減らすことができるものです。
普通に作ってみる
まずは例として、以下のようなTodoアプリを作りました。
コードとしては以下の通りです。
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;
上記をコンポーネントの図にすると以下の通りです。
上から、
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を受け取っています。
つまり、
以下の画像のようになっています。
そう、
このような設計にすれば、
なんと全くもってpropsのバケツリレーをすることがないのです。
つまり、
本来のTodoListItemの責務である、
todoのリストを表示することを従順に守ることができ、
余分に使わないpropsを受け取ることがなくなるんです。
なので、
値は全て最上部のコンポーネントから流し込まれることとなり、
値が追いにくい!という状況は打破できるのです。
しかもこうすれば、
propsを大量に受け渡すことによるパフォーマンスの低下も避けられます。
(Reactはpropsに変化がないか見比べるのにリソースを使うため)
これで、
いい感じにTodoアプリを実装できました!
ありがとうございます!
最後に
今回紹介したCompositionはReactの公式ドキュメントのMain Conceptsの方に載ってます。
TypeScriptを使うと、
ある程度は値は追いやすくなると思います。
しかしそれでも規模が大きくなってくると、
コンポーネント自体の責務が曖昧になり、
開発効率を低下させてしまうことがあります。
そういった中で、
Compositionのようなものを使えば、
コンポーネントごとの責務、役割を明確にできる。
それによって開発効率を上げることができ、
チームのバリューが増すと思います。
なので、
常にそういったアンテナを貼り続けて
チーム全体としての価値を上げていけたらなと。
以上、ありがとうございました。