これから作るものはシンプルなToDoリストのUIだ。
ざくっとsrc/index.jsに書いて、とりあえずChromeで表示してみよう。
src/index.jsの中身を消して次のコードを書いてみて。
import React from 'react';
import ReactDOM from 'react-dom';
const render = () => {
ReactDOM.render(<App />, document.getElementById('root'));
};
const App = () => (
<div>
<h1>やること</h1>
<p>
<input type="text" placeholder="やることある?" />
</p>
<ul>
<li>
<label>
<input type="checkbox" />
<span>#3 Reactチュートリアル</span>
</label>
</li>
<li>
<label>
<input type="checkbox" />
<span>#2 JavaScriptチュートリアル</span>
</label>
</li>
<li>
<label>
<input type="checkbox" checked="checked" />
<span>#1 環境構築</span>
</label>
</li>
</ul>
<p>
<button>終わったやつ消す</button>
</p>
</div>
);
render();Chromeには次のような画面が表示されるはず。
これからこの画面に対して次のような修正をしていく。
- ToDoを配列で持つようにする
- 新しいToDoを追加できるようにする
- チェックを付けたり外したりできるようにする
- ボタンをすとチェックを付けたToDoを消せるようにする
- コンポーネントを分割する
- ファイルを分割する
- Fluxを導入する
- React Routerを導入する
まずはToDoを配列で持つように修正してみよう。
今は3つのToDoをli要素でハードコーディングしている。
ToDoは「やる内容を表すテキスト」と「やったかどうかを表すフラグ」を持っている。
また、#1というふうに表示されているIDを持っている。
これをクラスで表現してみよう。
class Todo {
constructor(id, content, done) {
this.id = id;
this.content = content;
this.done = done;
}
}このTodoクラスのインスタンスを配列で持つようにする。
//Hello, world!のときのmessageと同じ理由でletを使用している。
let todoList = [
new Todo(3, '環境構築', false),
new Todo(2, 'JavaScriptチュートリアル', false),
new Todo(1, 'Reactチュートリアル', true)
];li要素をtodoListから組み立てよう。
<ul>
{todoList.map(todo => (
<li key={todo.id}>
<label>
<input type="checkbox" checked={todo.done} />
<span>#{todo.id} {todo.content}</span>
</label>
</li>
))}
</ul>Todoクラスの配列であるtodoListを、mapメソッドを使ってJSXで書かれたli要素へ変換している。
li要素にkeyという属性が付与されているが、これはReactがこのli要素を追跡するために必要なものだ。
このように配列をもとに複数の要素を書き出すような場合は可能な限りkey属性を付けるようにしておこう。
これでToDoを配列で持つようになった。
ToDoを配列で持つようにしたので、そこに新しいToDoを追加できるようにしよう。
テキストをinput要素で入力し、Enterを押すと追加される、というUIにする。
まず、input要素に表示されるテキストを表す変数を導入する。
//Hello, world!のときのmessageと同じ理由でletを使用している。
let content = '';<p>
<input type="text" placeholder="やることある?" value={content} />
</p>ここで書いているvalue={content}というコードはあくまでも変数contentをinput要素のvalue属性に設定するということだけを表すので、このコードでinput要素への入力がcontentに代入されることはない。
onChangeイベントを使用してinput要素への入力を変数contentへと反映させるようにしよう。
const updateContent = event => {
content = event.target.value;
render();
};<p>
<input type="text" placeholder="やることある?" value={content}
onChange={updateContent} />
</p>あとはEnterを押すとToDoの配列に追加すればよい。
onKeyPressイベントでEnterが押されたときに処理を行おう。
<p>
<input type="text" placeholder="やることある?" value={content}
onChange={updateContent} onKeyPress={tryAddTodo}/>
</p>let idGenerator = 3;
const tryAddTodo = event => {
if (content !== '' && event.key === 'Enter') {
const id = ++idGenerator;
const todo = new Todo(id, content, false);
todoList = [todo, ...todoList];
content = '';
render();
}
};todoListを変更しているコードをよく見てみよう。
todoList = [todo, ...todoList];todoListへ新しい配列が代入されている。
右辺は全体が[と]で囲まれており、新しく作られたtodoと...todoListというものが,で区切られている。
この...todoListの...はスプレッド演算子と言い、配列(またはオブジェクト)を展開するものだ。
ちょっとここでREPLを使ってスプレッド演算子の動作を見てみよう。
新しくコマンドプロンプトを起動してnodeと打ってREPLを起動しよう。
例として「3つの引数を受け取って合計して返す」関数を書いてみよう。
const sum = (a, b, c) => a + b + c;動きも確認してみよう。
sum(1, 2, 3); //6
const x = 4, y = 5, z = 6;
sum(x, y, z); //15ここで関数sumへ要素が3つの配列を渡したい場合、どのように書けるだろうか。
const ns = [7, 8, 9];
//そのままは渡せない!(正確に言うと渡せるけど期待しない結果になる)
sum(ns); //'7,8,9undefinedundefined'
//煩雑すぎる!
sum(ns[0], ns[1], ns[2]); //24こういうときに役立つのがスプレッド演算子だ。
sum(...ns); //24先ほど「配列を展開する」と述べた意味が体感できただろうか。
スプレッド演算子は配列リテラルでも使える。
const ms = [3, 4, 5];
//これだと長さが3の配列で最後の要素が入れ子になった配列になってしまう!
[1, 2, ms]; //[1, 2, [3, 4, 5]]
//スプレッド演算子で展開すればよい
[1, 2, ...ms]; //[1, 2, 3, 4, 5]スプレッド演算子は配列だけじゃなくてオブジェクトに対しても使えるんだけど、実際に出てきたときにまた話題にしよう。
それではREPLを閉じて画面作りに戻ろう。
各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>ToDoは複数あるので、どれが対象なのか分かるようにidを渡してupdateStatus関数を呼び出している。
第2引数にはチェック状態を渡している。
updateStatus関数では受け取った情報をもとにToDoリストの状態を更新する。
const updateStatus = (id, done) => {
todoList = todoList.map(todo => {
if (todo.id === id) {
return new Todo(todo.id, todo.content, done);
}
return todo;
});
render();
};todoListをmapメソッドで変換するが、対象のtodoは渡されたdoneで状態を変更している。
変更対象でなければそのままtodoを返して新しいtodoListを作っている。
新しいTodoインスタンスを返しているコードは少し煩雑だ。
return new Todo(todo.id, todo.content, done);
第1引数も第2引数もTodoクラスのメンバを参照している。
この「doneだけを変更して新しいTodoインスタンスを作る処理」はTodoクラスに持たせたほうがよさそうだ。
TodoクラスにsetDoneメソッドを追加する。
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);
}
}updateStatus関数ではこのsetDoneメソッドを使うようにする。
const updateStatus = (id, done) => {
todoList = todoList.map(todo => {
if (todo.id === id) {
return todo.setDone(done);
}
return todo;
});
render();
};コードが少しスッキリしたかな。
このリファクタリングでは「doneの状態を変更する」処理をTodoクラスに持たせることで処理の詳細をカプセル化した。
もしかしたら、if (todo.id === id)もTodoクラスに持っていけばmapメソッドに渡している関数をもっとシンプルにできるんじゃないか、という意見もあるだろう。
私は「doneの状態を変える対象がどのtodoなのか判断する責務」はTodoクラスではなくupdateStatus関数が持った方が自然だと考えたので、このようなリファクタリングにした。
さて、完了したToDoがいつまでも残っていても仕方がないので消せるようにしよう。
これまでと同じ要領でボタンが押されたイベントをハンドリングして、未完了のToDoだけの配列を変数todoListへ再代入した後に再描画をすればよい。
ここらで一度、コード例を見る前に自力で実装してみよう。
実装できたら答え合わせしよう。 コード例は次の通り。
<p>
<button onClick={clear}>終わったやつ消す</button>
</p>const clear = event => {
todoList = todoList.filter(todo => todo.done === false);
render();
};ここまでTodoインスタンスを配列で持っていたけれど、これをクラスにしよう。
型を大切にしているプログラマでもリストやマップなどのコレクションはそのまま使ってしまいがちだ。
抽象的なコレクション型で表現された状態をカプセル化して定義される具体的なコレクション型を「ファーストクラスコレクション」と呼ぶ。
それではコードを修正していこう。
まず、現在の表現であるTodoの配列をメンバに持つクラスを定義しよう。
class TodoList {
constructor(list) {
this.list = list;
}
}変数todoListに対して、
- 新しい
Todoインスタンスの追加 idを指定してdoneを変更- 完了した
Todoを除く
といった操作がされている。
これらをTodoListクラスのメソッドにする。
class TodoList {
constructor(list) {
this.list = list;
}
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));
}
}あとは変数todoListを操作しているところを、TodoListのメソッドを使うように修正すればよい。
更にTodoインスタンスのファクトリメソッドを定義した。
これによりidGeneratorがTodoクラスへカプセル化された。
ここまでのコードを次に示す。
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 App = () => (
<div>
<h1>やること</h1>
<p>
<input type="text" placeholder="やることある?" value={content}
onChange={updateContent} onKeyPress={tryAddTodo} />
</p>
<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>
);
render();