React を使って TODO サンプルを TypeScript で書き直したので、 今度はさらに Redux も使ってみる。
まずは Redux の型定義ファイルをインストール。
tsd query redux --action install --resolve tsd query react-redux --action install --resolve
特にひっかかることなく、スンナリと実装できた。
/// <reference path="typings/tsd.d.ts" /> import * as React from "react" import * as ReactDOM from "react-dom" import { createStore, combineReducers, Dispatch } from "redux" import { Provider, connect } from "react-redux" import * as _ from "lodash" // タスクのインタフェース interface ITask { name?: string; completed?: boolean; editing?: boolean; } // タスクを追加するときに呼び出すコールバックのインタフェース interface ITaskAddCallback { (task: ITask): void; } // タスクを保存するときに呼び出すコールバックのインタフェース interface ITaskSaveCallback { (task: ITask, newValue: ITask): void; } // タスクを削除するときに呼び出すコールバックのインタフェース interface ITaskDeleteCallback { (task: ITask): void; } // アクションの種類を定義 enum TaskActionType { Add = 1, Save, Delete } // タスクのアクションを表すインタフェース interface ITaskAction { type: TaskActionType; task?: ITask; values?: ITask; } // タスクを追加するアクションを作成する function addTask(task: ITask): ITaskAction { return { type: TaskActionType.Add, task: task }; } // タスクを保存するアクションを作成する function saveTask(task: ITask, newValues: ITask): ITaskAction { return { type: TaskActionType.Save, task: task, values: newValues }; } // タスクを削除するアクションを作成する function deleteTask(task: ITask): ITaskAction { return { type: TaskActionType.Delete, task: task }; } // タスクを操作する Reducer function tasksReducers(state: ITask[], action: ITaskAction): ITask[] { state = state || [ { name: "foo", completed: false, editing: false }, { name: "bar", completed: false, editing: false }, { name: "hoge", completed: false, editing: false }, { name: "fuga", completed: false, editing: false } ]; switch (action.type) { case TaskActionType.Add: return state.concat(action.task); case TaskActionType.Save: return state.map(t => { if (t == action.task) { return _.assign({}, t, action.values); } else { return t; } }); case TaskActionType.Delete: return state.filter(t => t != action.task); default: return state; } } // タスクを表示するコンポーネントの props のインタフェース interface ITaskProps { task: ITask; onTaskSave: ITaskSaveCallback; onTaskDelete: ITaskDeleteCallback; } // タスクを表示するコンポーネント class Task extends React.Component<ITaskProps, any> { private handleEdit = (e) => { this.props.onTaskSave(this.props.task, { editing: true }); } private handleDelete = (e) => { this.props.onTaskDelete(this.props.task); } private handleChangeCheckbox = (e) => { this.props.onTaskSave(this.props.task, { completed: !this.props.task.completed }); } render(): JSX.Element { return ( <div> <input type="checkbox" checked={this.props.task.completed} onChange={this.handleChangeCheckbox} /> <span>{this.props.task.name}</span> <a href="#" onClick={this.handleEdit}>[edit]</a> <a href="#" onClick={this.handleDelete}>[del]</a> </div> ); } } // タスクを編集するコンポーネントの props のインタフェース interface IEditTaskFormProps { task: ITask; onTaskSave: ITaskSaveCallback; } // タスクを編集するコンポーネントの state のインタフェース interface IEditTaskFormState { name: string; } // タスクをその場編集するためのコンポーネント class EditTaskForm extends React.Component<IEditTaskFormProps, IEditTaskFormState> { constructor(props: IEditTaskFormProps) { super(props); this.state = { name: "" }; } private handleCancel = (e) => { this.props.onTaskSave(this.props.task, { editing: false }); } private handleSave = (e) => { e.preventDefault(); if (!this.state.name) { return; } this.props.onTaskSave(this.props.task, { name: this.state.name, editing: false }); this.setState({ name: "" }); } private handleChangeText = (e) => { this.setState({ name: e.target.value }); } render(): JSX.Element { return ( <div> <input type="text" value={this.state.name || this.props.task.name} onChange={this.handleChangeText} ref="name"/> <button onClick={this.handleSave}>save</button> <button onClick={this.handleCancel}>cancel</button> </div> ); } } // タスク一覧を表示するコンポーネントの props のインタフェース interface ITaskListProps { tasks: ITask[]; onTaskSave: ITaskSaveCallback; onTaskDelete: ITaskDeleteCallback; } // タスク一覧を表示するためのコンポーネント class TaskList extends React.Component<ITaskListProps, any> { render(): JSX.Element { var rows = this.props.tasks.map(t => { if (t.editing) { return ( <EditTaskForm task={t} onTaskSave={this.props.onTaskSave} /> ); } else { return ( <Task task={t} onTaskSave={this.props.onTaskSave} onTaskDelete={this.props.onTaskDelete} /> ); } }); return ( <div> {rows} </div> ); } } // タスク追加フォームの props のインタフェース interface IAddTaskFormProps { onTaskAdd: ITaskAddCallback; } // タスク登録フォームを表示するためのコンポーネント class AddTaskForm extends React.Component<IAddTaskFormProps, any> { private handleAdd = (e) => { e.preventDefault(); var newTask = ReactDOM.findDOMNode<HTMLInputElement>(this.refs["name"]).value.trim(); if (!newTask) { return; } this.props.onTaskAdd({ name: newTask, completed: false, editing: false }); ReactDOM.findDOMNode<HTMLInputElement>(this.refs["name"]).value = ""; } render() { return ( <div> <input type="text" placeholder="newTask" ref="name"/> <button onClick={this.handleAdd}>add</button> </div> ); } } // ルートコンポーネントの state のインタフェース interface ITaskAppProps { tasks?: ITask[]; dispatch?: Dispatch; } // ルートコンポーネント // 状態をまとめて管理する class _TaskApp extends React.Component<ITaskAppProps, any> { private handleTaskAdd = (newTask: ITask): void => { this.props.dispatch(addTask(newTask)); } private handleTaskDelete = (task: ITask): void => { this.props.dispatch(deleteTask(task)); } private handleTaskSave = (task: ITask, newValues: ITask): void => { this.props.dispatch(saveTask(task, newValues)); } render(): JSX.Element { return ( <div> <AddTaskForm onTaskAdd={this.handleTaskAdd} /> <TaskList tasks={this.props.tasks} onTaskSave={this.handleTaskSave} onTaskDelete={this.handleTaskDelete} /> </div> ); } } const TaskApp = connect(state => { return { tasks: state.tasks }; })(_TaskApp); // ストアに渡す Reducer を作成 const rootReducer = combineReducers({ tasks: tasksReducers }); // 唯一のストアを作成 const store = createStore(rootReducer); ReactDOM.render( <Provider store={store}> <TaskApp /> </Provider>, document.getElementById("content") );
コンテナコンポーネントが持っていた状態を Redux のストアに持つようにした。 それにともない、コンテナコンポーネントは状態を操作するのではなく、アクションを呼び出すだけに変更。 Redux の枠組みにのっとって、状態は Reducer が操作している。
あと、アクションの種類に enum を使ってみた。 もし複数の enum で定義する場合は、明示的に開始値を指定して値の範囲を分けておかないと、 複数の Reducer で処理されてしまいそう。 素直に文字列で定義するか、1 つの enum にアクションの種類をすべて詰め込んでいいかもな。