先日 React で作ったタスクリストのサンプルを、 React と Redux で書き直してみた。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Redux Sample</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.0.4/redux.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.0.0/react-redux.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> </head> <body> <div id="content"></div> <script type="text/babel"> // アクションの種類を定義 const ADD_TASK = "ADD_TASK"; const SAVE_TASK = "SAVE_TASK"; const DELETE_TASK = "DELETE_TASK"; // Action Creator を定義 function addTask(name) { return { type: ADD_TASK, name }; } function saveTask(task, values) { return { type: SAVE_TASK, task, values }; } function deleteTask(task) { return { type: DELETE_TASK, task }; } // 最初に表示するタスク const initialState = [ { name: "foo", completed: false, editing: false }, { name: "bar", completed: false, editing: false }, { name: "hoge", completed: false, editing: false }, { name: "fuga", completed: false, editing: false } ]; // Reducer を定義 function taskReducer(state = initialState, action) { switch (action.type) { case ADD_TASK: return state.concat({ name: action.name, completed: false, editing: false }); case SAVE_TASK: return state.map(t => { if (t === action.task) { return $.extend(action.task, action.values); } else { return t; } }); case DELETE_TASK: return state.filter(t => { return t !== action.task; }); default: // この Reducer で処理しないアクションのときは // state をそのまま返さないといけない return state; } } const rootReducer = Redux.combineReducers({ tasks: taskReducer }); // タスク表示用コンポーネントを定義 var Task = React.createClass({ handleEdit(e) { this.props.dispatch(saveTask(this.props.task, { editing: true })); }, handleDelete(e) { this.props.dispatch(deleteTask(this.props.task)); }, handleChangeCheckbox(e) { this.props.dispatch(saveTask(this.props.task, { completed: !this.props.task.completed })); }, render() { return ( <div> <input type="checkbox" checked={this.props.task.completed} onChange={this.handleChangeCheckbox} /> {(() => { if (this.props.task.completed) { return <del>{this.props.task.name}</del> } else { return <span>{this.props.task.name}</span> } }())} <a href="#" onClick={this.handleEdit}>[edit]</a> <a href="#" onClick={this.handleDelete}>[del]</a> </div> ); } }); // タスク編集用コンポーネントを定義 var EditTaskForm = React.createClass({ getInitialState() { return { name: "" }; }, handleCancel(e) { this.props.dispatch(saveTask(this.props.task, { editing: false })); }, handleSave(e) { e.preventDefault(); if (!this.state.name) { return; } this.props.dispatch(saveTask(this.props.task, { name: this.state.name, editing: false })); this.setState({ name: "" }); }, handleChangeText(e) { this.setState({ name: e.target.value }); }, render() { 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> ); } }); // タスクリスト表示用コンポーネントを定義 var TaskList = React.createClass({ render() { var rows = this.props.tasks.map(t => { if (t.editing) { return ( <EditTaskForm task={t} dispatch={this.props.dispatch} /> ); } else { return ( <Task task={t} dispatch={this.props.dispatch} /> ); } }); return ( <div> {rows} </div> ); } }); // タスク登録用コンポーネントを定義 var AddTaskForm = React.createClass({ handleAdd(e) { e.preventDefault(); var newTask = ReactDOM.findDOMNode(this.refs.name).value.trim(); if (!newTask) { return; } this.props.dispatch(addTask(newTask)); ReactDOM.findDOMNode(this.refs.name).value = ""; }, render() { return ( <div> <input type="text" placeholder="newTask" ref="name"/> <button onClick={this.handleAdd}>add</button> </div> ); } }); // ルートコンポーネントを定義 var _TaskApp = React.createClass({ handleTaskSave(task, values) { this.props.dispatch(saveTask(task,values)); }, handleTaskDelete(task) { this.props.dispatch(deleteTask(task)); }, render() { return ( <div> <AddTaskForm dispatch={this.props.dispatch} /> <TaskList tasks={this.props.tasks} dispatch={this.props.dispatch} /> </div> ); } }); // ルートコンポーネントをデコレート var TaskApp = ReactRedux.connect(state => { return { // state には Reducer を実行した結果が格納されているっぽい // combineReducers に Reducer を渡すときのキーと同名みたい tasks: state.tasks }; })(_TaskApp); // ストアを作成 var store = Redux.createStore(rootReducer); ReactDOM.render( <ReactRedux.Provider store={store}> <TaskApp /> </ReactRedux.Provider>, document.getElementById("content") ); </script> </body> </html>
React も Redux も Bebel も全部 script タグで CDN から読み込んでいる。 表示に時間はかかるけど、コピペで試せてお手軽。 cdnjs 便利だ。
Redux を使っていて思ったことをざっと書いておこう。
- state は Redux のストアが一元管理する
- Redux のストアが state を管理するから React コンポーネントはほとんど state を使わないな
- アクションは単なるオブジェクト
- Action Creator はそのアクションを作るだけの関数
- state を変更するのは Reducer の役割
- Reducer は state とアクションを受け取って、新しい state を返す関数
- React コンポーネントの自体のコード量は state を使わないぶん少し減った程度
末端のコンポーネントからコンテナコンポーネントまでイベント(というかコールバック)を伝播して、 コンテナコンポーネントで dispatch を呼び出してみたけど、 これ末端のコンポーネントで dispatch 呼び出したらダメなんだろうか。
Middleware はまだ試していないし、Web API 呼び出しをどこに書けばいいかもまだ掴めていない。 ルーティングは react-router を使えばよさそうかな。
React + Redux なかなか良い感じ。