React ComponentのPropsには好きなものを渡せる
使いやすいReact Componentを作るために、何をPropsとして渡せば良いか考えたことはありますか?
Propsに何でも渡せるのがReact Componentの良さですが、自由度が高いために何も考えないと使いにくいコンポーネントになってしまいます。
使いにくいコンポーネントの例
TODOリストのアプリの新しいTODOを追加する部分を思い浮かべて下さい。イメージとしてTODO MVCを拝借しますが、この部分です。
TODO MVCのコードでは無いことを断っておきますが、コードはこんな感じでしょうか。APIとStoreは適当にRESTやReduxを思い浮かべてくれれば良いです。
import { useState } from 'react';
import { postTodoInAPI } from '@api';
const NewTodoForm = ({ addTodo, postTodoInAPI }) => {
const [title, setTitle] = useState('');
const handleChange = (e) => setTitle(e.target.value);
const handleSubmit = (e) => {
e.preventDefault();
const newTodo = { title };
postTodoInAPI(newTodo);
addTodo(newTodo);
};
return (
<form onSubmit={handleSubmit}>
<input name='title' value={title} onChange={handleChange} />
</form>
);
};
const TodoList = () => {
const { todos, addTodo } = useStore();
return (
<div>
<NewTodoForm addTodo={addTodo} postTodoInAPI={postTodoInAPI} />
<ul>
{todos.map(({ title }, i) => (
<li key={i}>{title}</li>
))}
</ul>
</div>
);
};
このコードにはPropsの渡し方のせいで使いにくいコンポーネントになっている部分があります。
NewTodoForm
に addTodo
と postTodoInAPI
を渡しているせいで、TODOを追加するときのロジックを NewTodoForm
の中で組み立てています。このせいで NewTodoForm
がAPIとStoreに依存するようになってしまいます。
例えば、インフラのコスト削減のためにDBを廃止して、代わりに localStorage
を使うようになったとします。APIが不要になるので postTodoInAPI
を廃止して、代わりに addTodoInLocalStorage
を使うようになります。
import { useState } from 'react';
- import { postTodoInAPI } from '@api';
+ import { addTodoInLocalStorage } from '@localstorage';
- const NewTodoForm = ({ addTodo, postTodoInAPI }) => {
+ const NewTodoForm = ({ addTodo, addTodoInLocalStorage }) => {
const [title, setTitle] = useState('');
const handleChange = (e) => setTitle(e.target.value);
const handleSubmit = (e) => {
e.preventDefault();
const newTodo = { title };
- postTodoInAPI(newTodo);
+ addTodoInLocalStorage(newTodo);
addTodo(newTodo);
};
return (
<form onSubmit={handleSubmit}>
<input name='title' value={title} onChange={handleChange} />
</form>
);
};
const TodoList = () => {
const { todos, addTodo } = useStore();
return (
<div>
- <NewTodoForm addTodo={addTodo} postTodoInAPI={postTodoInAPI} />
+ <NewTodoForm addTodo={addTodo} addTodoInLocalStorage={addTodoInLocalStorage} />
<ul>
{todos.map(({ title }, i) => (
<li key={i}>{title}</li>
))}
</ul>
</div>
);
};
このときに NewTodoForm
と TodoList
の2つのコンポーネントで改修が発生していまいます。NewTodoForm
を別のコンポーネントでも使っていた場合、そこに影響しないように注意を払わなければならず、最悪の場合 NewTodoForm
を共通で使えなくなってしまうかもしれません。
ここで言えるのは NewTodoForm
は再利用しやすいコンポーネントではなかったということです。それは責務を考えずに NewTodoForm
にPropsを渡していたからです。
使いやすいコンポーネントの例
NewTodoForm
の責務は「新しいTODOを追加すること」であり、新しいTODOをどこ(DB or localStorage
)に追加するかは NewTodoForm
の関与するところでは無いのです。NewTodoForm
は新しいTODOを追加するためのfunctionを知っていれば良く、新しいTODOをAPIや localStorate
や Store に追加するfunctionは知る必要がないのです。
それはどういうコードなのかというと、こうなります。
import { useState } from 'react';
import { postTodoInAPI } from '@api';
- const NewTodoForm = ({ addTodo, postTodoInAPI }) => {
+ const NewTodoForm = ({ onAdd }) => {
const [title, setTitle] = useState('');
const handleChange = (e) => setTitle(e.target.value);
const handleSubmit = (e) => {
e.preventDefault();
const newTodo = { title };
- addTodo(newTodo);
- postTodoInAPI(newTodo);
+ onAdd(newTodo);
};
return (
<form onSubmit={handleSubmit}>
<input name='title' value={title} onChange={handleChange} />
</form>
);
};
const TodoList = () => {
const { todos, addTodo } = useStore();
+ const handleAdd = (newTodo) => {
+ postTodoInAPI(newTodo);
+ addTodo(newTodo);
+ }
+
return (
<div>
- <NewTodoForm addTodo={addTodo} postTodoInAPI={postTodoInAPI} />
+ <NewTodoForm onAdd={handleAdd} />
<ul>
{todos.map(({ title }, i) => (
<li key={i}>{title}</li>
))}
</ul>
</div>
);
};
これならAPIから localStorage
に乗り換えても TodoList
の handleAdd
を変えるだけで、 NewTodoForm
には影響しません。
import { useState } from 'react';
- import { postTodoInAPI } from '@api';
+ import { addTodoInLocalStorage } from '@localstorage';
const NewTodoForm = ({ onAdd }) => {
const [title, setTitle] = useState('');
const handleChange = (e) => setTitle(e.target.value);
const handleSubmit = (e) => {
e.preventDefault();
const newTodo = { title };
onAdd(newTodo);
};
return (
<form onSubmit={handleSubmit}>
<input name='title' value={title} onChange={handleChange} />
</form>
);
};
const TodoList = () => {
const { todos, addTodo } = useStore();
const handleAdd = (newTodo) => {
- postTodoInAPI(newTodo);
+ addTodoInLocalStorage(newTodo);
addTodo(newTodo);
}
return (
<div>
<NewTodoForm onAdd={handleAdd} />
<ul>
{todos.map(({ title }, i) => (
<li key={i}>{title}</li>
))}
</ul>
</div>
);
};
終わりに
今回はシンプルの例だったので重要性が伝わりにくかったかもしれないですが、このような小さな積み重ねが日々の開発効率の改善に繋がります。せっかくReactをつかっているのですから、コンポーネントは再利用しやすいようにしておくに越したことはないのではないでしょうか。