先日 React で作ったタスクリストのサンプルを、
React と Redux で書き直してみた。
<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";
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 }
];
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:
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 {
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 なかなか良い感じ。