コンポーネントを分割したので、次はファイルも分割しよう。 ここでは次のような単位で分割する。
- モデル(
Todoクラス、TodoListクラス) - データ(
content、todoList) - アクション(
updateContent,tryAddTodo,updateStatus,clear) - 独立したコンポーネント(
InputContent,TodoComponent,TodoListComponent,ClearButton) - 構造化するコンポーネント(
App) - 描画関数(
render)
TodoクラスもTodoListクラスも他の単位への依存がない(つまり、モデルはデータやアクション・コンポーネントへ依存していない、ということ)のでモデルをファイルに分割するのは簡単だ。
次の内容をsrc/models.jsに保存する。
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));
}
}
export { Todo, TodoList };クラス定義はsrc/index.jsからそのまま持ってきた。
追加されているコードは最終行だ。
export { Todo, TodoList };これはTodoクラスとTodoListクラスを他のファイルからも使えるようにするためのコードだ。
src/index.jsからはTodoクラスとTodoListクラスを定義していたコードがなくなり、代わりにimportが追加される。
import { Todo, TodoList } from './models';データの分割は少しだけ面倒だ。
次の内容をsrc/store.jsに保存する。
import { Todo, TodoList } from './models';
let todoList = TodoList.empty()
.add(Todo.create('環境構築').setDone(true))
.add(Todo.create('JavaScriptチュートリアル'))
.add(Todo.create('Reactチュートリアル'));
let content = '';
const store = { todoList, content };
export default store;データはTodoクラスとTodoListクラスに依存しているのでsrc/model.jsをimportしている。
それからtodoListとcontentを1つのオブジェクトにまとめている。
ちなみに、オブジェクトリテラルはキーと値のペアを,で区切って設定するのだけど、キーを省略すると値が代入されている変数の名前をキーとみなしてくれる。
つまり次のコードは、
const store = { todoList, content };次のコードと等価だ。
const store = { todoList: todoList, content: content };このstoreをexportしている。
export default store;変数contentと変数todoListを参照していた箇所はstore.を付けるように修正する。
storeのexportにはTodoクラスとTodoListクラスのexportのときにはなかったdefaultというキーワードが付いている。
defaultが付いているほうをデフォルトエクスポート、defaultが付いていないほうを名前付きエクスポートと呼ぶ。
デフォルトエクスポートは1ファイルにつき1つの値のみexportできる。
importするときはファイルを指定すればよい。
export default 'hello world';
import Greeting from './hello';名前付きエクスポートは1ファイルにつき複数の値をexportできる。
importするときは名前を指定しなければいけない。
const foo = 'FOO';
const bar = 12345;
export { foo, bar };
//宣言と同時にexportできる
export const baz = [1, 2, 3];
import { foo, bar, baz } from './foobar';アクションは再描画のためにrender関数を呼び出す。
つまりrender関数を渡さないといけない。
次の内容をsrc/actions.jsへ保存する。
import { Todo } from './models';
import store from './store';
let render;
export const updateContent = event => {
store.content = event.target.value;
render();
};
export const tryAddTodo = event => {
if (store.content !== '' && event.key === 'Enter') {
const todo = Todo.create(store.content);
store.todoList = store.todoList.add(todo);
store.content = '';
render();
}
};
export const updateStatus = (id, done) => {
store.todoList = store.todoList.setDone(id, done);
render();
};
export const clear = event => {
store.todoList = store.todoList.clear();
render();
};
export default (h) => {
render = h;
};デフォルトエクスポートしている関数がrender関数を設定するためのものだ。
アクションを使う側は次のようにimportした後に、
import initRender, { updateContent, tryAddTodo, updateStatus, clear } from './actions';render関数を渡す必要がある。
initRender(render);ファイル分割もかなり進んできた。
次は独立したコンポーネント(つまりApp以外のもの)を分割する。
次の内容をsrc/components.jsへ保存する。
import React from 'react';
export 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>
);
};
export 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>
);
};
export const TodoListComponent = (props) => (
<ul>
{props.children}
</ul>
);
export const ClearButton = (props) => (
<p>
<button onClick={props.clear}>終わったやつ消す</button>
</p>
);独立したコンポーネントを分割できたら次は構造化するコンポーネントだ。
次の内容をsrc/component-structure.jsへ保存する。
import React from 'react';
import store from './store';
import { updateContent, tryAddTodo, updateStatus, clear } from './actions';
import { InputContent, TodoListComponent, TodoComponent, ClearButton } from './components';
const App = () => (
<div>
<h1>やること</h1>
<InputContent
content={store.content}
updateContent={updateContent}
tryAddTodo={tryAddTodo} />
<TodoListComponent>
{store.todoList.list.map(todo => (
<TodoComponent
key={todo.id}
todo={todo}
updateStatus={updateStatus} />
))}
</TodoListComponent>
<ClearButton clear={clear} />
</div>
);
export default App;最後に描画関数まわりを整えよう。
次の内容をsrc/index.jsへ保存する。
import React from 'react';
import ReactDOM from 'react-dom';
import initRender from './actions';
import App from './component-structure';
const render = () => {
ReactDOM.render(<App />, document.getElementById('root'));
};
initRender(render);
render();