ざっくり書いた画面はAppという1つのコンポーネントだけで構成されていた。
これを次のように分割したい。
- テキスト入力
- ToDo一覧
- 1つのTodo
- 完了Todoを消すボタン
テキスト入力部分を分割してみよう。
const InputContent = () => (
<p>
<input type="text" placeholder="やることある?" value={content}
onChange={updateContent} onKeyPress={tryAddTodo} />
</p>
);
const App = () => (
<div>
<h1>やること</h1>
<InputContent />
<ul>
{todoList.list.map(todo => (
<li key={todo.id}>
<label>
<input type="checkbox" checked={todo.done}
onChange={event => updateStatus(todo.id, event.target.checked)} />
<span>#{todo.id} {todo.content}</span>
</label>
</li>
))}
</ul>
<p>
<button onClick={clear}>終わったやつ消す</button>
</p>
</div>
);Appと同じようにInputContentという変数にテキスト入力部を分割した。
App側ではdivやpといった要素と同じようにInputContentをタグで追加している。
同じようにToDo一覧とボタンも分割しよう。
const TodoListComponent = () => (
<ul>
{todoList.list.map(todo => (
<li key={todo.id}>
<label>
<input type="checkbox" checked={todo.done}
onChange={event => updateStatus(todo.id, event.target.checked)} />
<span>#{todo.id} {todo.content}</span>
</label>
</li>
))}
</ul>
);
const ClearButton = () => (
<p>
<button onClick={clear}>終わったやつ消す</button>
</p>
);
const App = () => (
<div>
<h1>やること</h1>
<InputContent />
<TodoListComponent />
<ClearButton />
</div>
);TodoListクラスが既に存在するのでToDo一覧はTodoListComponentという名前にしてある。
次は1つのToDoを分割する。
これまでのように単純に分割するとtodoを解決できずエラーになる。
//これはエラーになるコード!
//"'todo' is not defined no-undef"というエラーが出る!
const TodoComponent = () => (
<li>
<label>
<input type="checkbox" checked={todo.done}
onChange={event => updateStatus(todo.id, event.target.checked)} />
<span>#{todo.id} {todo.content}</span>
</label>
</li>
);todoはtodoList.listをmapしているところで渡さなくてはいけない。
Reactでこのような値の受け渡しをするにはpropsが使える。
TodoComponentは次のようになる。
const TodoComponent = (props) => {
const todo = props.todo;
return (
<li>
<label>
<input type="checkbox" checked={todo.done}
onChange={event => updateStatus(todo.id, event.target.checked)} />
<span>#{todo.id} {todo.content}</span>
</label>
</li>
);
};これまでJSXで書かれたコンポーネントは引数が0個のアロー関数で表現されていたが、TodoComponentは引数propsを受け取っている。
この引数を経由してコンポーネントの親子間で値を受け渡せる。
TodoComponentにtodoを渡すため、TodoListComponentは次のようにする
const TodoListComponent = () => (
<ul>
{todoList.list.map(todo => (
<TodoComponent key={todo.id} todo={todo} />
))}
</ul>
);TodoComponentにtodo属性を設定しているが、これがTodoComponent側でprops.todoとして取り出せる。
TodoListComponentはtodoList.listのmapでTodoComponentを使ってToDoリストを構築している。
これはTodoListComponentがTodoComponentに依存していることになるが、この依存は切り離せる。
TodoListComponentを次のように変更する。
const TodoListComponent = (props) => (
<ul>
{props.children}
</ul>
);次にAppを次のように変更する。
const App = () => (
<div>
<h1>やること</h1>
<InputContent />
<TodoListComponent>
{todoList.list.map(todo => (
<TodoComponent key={todo.id} todo={todo} />
))}
</TodoListComponent>
<ClearButton />
</div>
);これでTodoListComponentからTodoComponentへの依存を切り離せた。
このようにコンポーネント間はできるだけ疎結合にしてAppのようなルートコンポーネントで構造化させる方法もあり、おそらくこのやり方のほうが拡張性、保守性は高いと考えている(まだ実践はしていないので、これまでの経験からくる勘)。
コンポーネント間の依存が解消できたので、次は参照している値に関する依存を解消しよう。
今のコードはInputContentが変数content、updateContent、tryAddTodoに依存している。
これはつまりInputContentの外側のスコープにそれらの変数が存在しなければいけないということだ。
これもpropsを使用することで解消する。
InputContentを次のように変更する。
const InputContent = (props) => {
const content = props.content;
const updateContent = props.updateContent;
const tryAddTodo = props.tryAddTodo;
return (
<p>
<input type="text" placeholder="やることある?" value={content}
onChange={updateContent} onKeyPress={tryAddTodo} />
</p>
);
};それからAppを次のように変更する。
const App = () => (
<div>
<h1>やること</h1>
<InputContent
content={content}
updateContent={updateContent}
tryAddTodo={tryAddTodo} />
<TodoListComponent>
{todoList.list.map(todo => (
<TodoComponent todo={todo} />
))}
</TodoListComponent>
<ClearButton />
</div>
);先ほどのコンポーネント間の依存と同じように、各コンポーネントは外側のスコープに依存せず、構造化を担当するAppが必要な値を渡すようにした。
こうすることでコンポーネントの独立性が高まり、依存関係をAppに集約できた。
同じように他のコンポーネントも独立させた。
ここまでのコードを次に示す。
import React from 'react';
import ReactDOM from 'react-dom';
class Todo {
constructor(id, content, done) {
this.id = id;
this.content = content;
this.done = done;
}
setDone(done) {
return new Todo(this.id, this.content, done);
}
static idGenerator = 0;
static create(content) {
return new Todo(++Todo.idGenerator, content, false);
}
}
class TodoList {
constructor(list) {
this.list = list;
}
static empty() {
return new TodoList([]);
}
add(todo) {
return new TodoList([todo, ...this.list]);
}
setDone(id, done) {
return new TodoList(this.list.map(todo => {
if (todo.id === id) {
return todo.setDone(done);
}
return todo;
}));
}
clear() {
return new TodoList(this.list.filter(todo => todo.done === false));
}
}
//Hello, world!のときのmessageと同じ理由でletを使用している。
let todoList = TodoList.empty()
.add(Todo.create('環境構築').setDone(true))
.add(Todo.create('JavaScriptチュートリアル'))
.add(Todo.create('Reactチュートリアル'));
//Hello, world!のときのmessageと同じ理由でletを使用している。
let content = '';
const updateContent = event => {
content = event.target.value;
render();
};
const tryAddTodo = event => {
if (content !== '' && event.key === 'Enter') {
const todo = Todo.create(content);
todoList = todoList.add(todo);
content = '';
render();
}
};
const updateStatus = (id, done) => {
todoList = todoList.setDone(id, done);
render();
};
const clear = event => {
todoList = todoList.clear();
render();
};
const render = () => {
ReactDOM.render(<App />, document.getElementById('root'));
};
const InputContent = (props) => {
const content = props.content;
const updateContent = props.updateContent;
const tryAddTodo = props.tryAddTodo;
return (
<p>
<input type="text" placeholder="やることある?" value={content}
onChange={updateContent} onKeyPress={tryAddTodo} />
</p>
);
};
const TodoComponent = (props) => {
const todo = props.todo;
const updateStatus = props.updateStatus;
return (
<li>
<label>
<input type="checkbox" checked={todo.done}
onChange={event => updateStatus(todo.id, event.target.checked)} />
<span>#{todo.id} {todo.content}</span>
</label>
</li>
);
};
const TodoListComponent = (props) => (
<ul>
{props.children}
</ul>
);
const ClearButton = (props) => (
<p>
<button onClick={props.clear}>終わったやつ消す</button>
</p>
);
const App = () => (
<div>
<h1>やること</h1>
<InputContent
content={content}
updateContent={updateContent}
tryAddTodo={tryAddTodo} />
<TodoListComponent>
{todoList.list.map(todo => (
<TodoComponent
key={todo.id}
todo={todo}
updateStatus={updateStatus} />
))}
</TodoListComponent>
<ClearButton clear={clear} />
</div>
);
render();