TODOアプリの作成
Reduxを使ったエントリポイントを作成する
npm install --save redux
- エントリポイント(index.js)を作成
import { createStore } from 'redux'
const store = createStore(() => 'Hello Redux');
document.getElementById('contents').innerHTML = store.getState().toString();
npx browserify -t babelify index.js -o bundle3.js
npm-scriptsのnpm run build
を上記コマンドにしておくと便利。
- index.htmlで bundle3.jsを読み込む
<div id="contents"></div>
にHello Reduxが差し込まれる
Viewを実装する
- View(index.html)をとりあえず静的に作成
<div id="addTodo">
<input type="text" name="" id="">
<button type="button">追加</button>
</div>
<ul id="todoList">
<li>11111</li>
<li style="text-decoration: line-through">222222</li>
</ul>
<p id="links">
Show: <a href="#">All</a>, <a href="#">Active</a>, <a href="#">Completed</a>
</p>
ActionCreatorを実装する
- actions/index.jsにActionCreatorを定義
actionsディレクトリに格納するのが一般的。
let nextTodoId = 0;
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
}
};
export const toggleTodo = (id) => {
return {
type: 'TOGGLE_TODO',
id
}
};
export const setVisibilityFilter = (filter) => {
return {
type: 'SET_VISIBILITY_FILTER',
filter
}
};
// addTodo, toggleTodo, setVisibilityFilterがActionCreator=Actionを返す関数
エントリポイントでActionCreatorを使う
- エントリポイント(index.js)を修正(ViewとActionCreatorのつなぎこみ)
Viewにイベントがあった場合に「ActionCreatorからActionを取得しStoreに渡す」処理を追加する。
ReduxでStoreにActionを渡すには、Storeのdispatchメソッドを呼ぶ。
import { createStore } from 'redux'
import { addTodo, toggleTodo, setVisibilityFilter } from './actions/index'
const store = createStore(() => 'Hello Redux');
// 追加ボタンがクリックされたらActionCreator(addTodo)からActionを取得しStoreに渡す
document.querySelector('#addTodo button').addEventListener('click', () => {
store.dispatch(addTodo(document.querySelector('#addTodo input').value));
});
// リストがクリックされたらActionCreator(toggleTodo)からActionを取得しStoreに渡す
document.querySelectorAll('#todoList li').forEach((li, index) => {
li.addEventListener('click', () => {
store.dispatch(toggleTodo(index));
});
});
// フィルタリング用リンクがクリックされたらActionCreator(setVisibilityFilter)からActionを取得しStoreに渡す
document.querySelectorAll('#links a').forEach((a) => {
a.addEventListener('click', () => {
store.dispatch(setVisibilityFilter(a.textContent));
});
});
Reducerを実装する
- reducers/index.jsにReducerを定義
reducersディレクトリに格納するのが一般的。
ReducerにはStoreから現在のStateとActionが渡され、新たなStateが返される。
import { combineReducers } from 'redux'
// 1つのTODOを処理するためのReducer
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
};
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state;
}
return Object.assign({}, state, {
completed: !state.completed
});
default:
return state;
}
};
// 複数のTODOを処理するためのReducer
const todos = (states=[], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...states,
todo(undefined, action)
];
case 'TOGGLE_TODO':
return states.map(state => todo(state, action));
default:
return states;
}
};
// 表示状態を処理するためのReducer
const visibilityFilter = (state='SHOW_ALL', action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
};
const todoApp = combineReducers({
todos,
visibilityFilter
});
export default todoApp;
Reducerの動作確認をする
- Reducerの動作確認をするためのコード
import todoApp from './reducers/index'
// 初期化時のstateとactionは空オブジェクトでなければならない
const initialState = todoApp({}, {});
console.log(initialState);
const secondState = todoApp(initialState, {type: 'ADD_TODO', id: 1, text: 'First todo'});
console.log(secondState);
const thirdState = todoApp(secondState, {type: 'SET_VISIBILITY_FILTER', filter: 'Active'});
console.log(thirdState);
npx browserify -t babelify reducer_test.js -o reducer_test_bundle.js
でバンドルしてブラウザで確認。
動作結果
・初期化後
{
todos: [],
visibilityFilter: "SHOW_ALL"
}
・ADD_TODO後
{
todos: [
{ id: 1, text: "First todo", completed: false }
],
visibilityFilter: "SHOW_ALL"
}
・SET_VISIBILITY_FILTER後
{
todos: [
{ id: 1, text: "First todo", completed: false }
],
visibilityFilter: "Active"
}
エントリポイントでReducerを使う
- エントリポイント(index.js)を修正
ReducerをStoreに設定する。(createStore関数の引数にReducerを渡す)
import { createStore } from 'redux'
import { addTodo, toggleTodo, setVisibilityFilter } from './actions/index'
import todoApp from './reducers/index'
const store = createStore(todoApp);
// 追加ボタンがクリックされたらActionCreator(addTodo)からActionを取得しStoreに渡す
document.querySelector('#addTodo button').addEventListener('click', () => {
store.dispatch(addTodo(document.querySelector('#addTodo input').value));
});
// リストがクリックされたらActionCreator(toggleTodo)からActionを取得しStoreに渡す
document.querySelectorAll('#todoList li').forEach((li, index) => {
li.addEventListener('click', () => {
store.dispatch(toggleTodo(index));
});
});
// フィルタリング用リンクがクリックされたらActionCreator(setVisibilityFilter)からActionを取得しStoreに渡す
document.querySelectorAll('#links a').forEach((a) => {
a.addEventListener('click', () => {
store.dispatch(setVisibilityFilter(a.textContent));
});
});
ViewでReactを使う
npm install --save react react-dom react-redux prop-types
※ 書籍内ではprop-typesがないが、react15.5から別パッケージとなった
npm install --save-dev @babel/preset-react
※ 書籍内ではbabel-preset-reactとなっているが古い。
- .babelrcに@babel/preset-reactを追加
{
"presets": [
[
"@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}
],
"@babel/preset-react"
]
}
- li表示用コンポーネント(components/Todo.js)を作成
import React from 'react';
import PropTypes from 'prop-types';
class Todo extends React.Component {
render() {
return (
<li onClick={this.props.onClick} style={{ textDecoration: this.props.completed ? 'line-through' : 'none' }}>{this.props.text}</li>
)
}
}
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired,
};
export default Todo;
※ 書籍ではimport React, {PropTypes} from 'react';
となっているが古い。
- ul表示用コンポーネント(components/TodoList.js)を作成
import React from 'react';
import PropTypes from 'prop-types';
import Todo from './Todo'
class TodoList extends React.Component {
render() {
return (
<ul>
{this.props.todos.map(todo =>
<Todo key={todo.id} {...todo} onClick={() => this.props.onTodoClick(todo.id)}/>
)}
</ul>
)
}
}
TodoList.propTypes = {
todos: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired).isRequired,
onTodoClick: PropTypes.func.isRequired,
};
export default TodoList;
- フィルタリンク用コンポーネント(components/Link.js)を作成
import React from 'react';
import PropTypes from 'prop-types';
class Link extends React.Component {
render() {
return (
<a href="" onClick={e => {
e.preventDefault();
this.props.onClick();}
}>
{this.props.children}
</a>
)
}
Link.propTypes = {
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired,
};
export default Link;
ReactとReduxのつなぎこみ
- containers/AddTodo.jsを作成(入力部分のつなぎこみ)
つなぎこみ部分はcontainersディレクトリに格納するのが一般的。
コードは長くなるが、行っていることは基本的には以下の2つのことだけ。
- Viewで発生したイベントをStoreに渡す
- Stateが変更されたことをViewが受け取る
import React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';
class AddTodo extends React.Component {
render() {
let input;
return (
<div>
<form onSubmit={e => {
e.preventDefault();
if (!input.value.trim()) {
return;
}
// ActionCreatorからActionを取得してStoreに渡す
this.props.dispatch(addTodo(input.value));
input.value = '';
}}>
<input ref={node => {
input = node;
}} />
<button type="submit">追加</button>
</form>
</div>
)
}
}
export default connect()(AddTodo);
- containers/VisibleTodoList.jsを作成(リスト部分のつなぎこみ)
import { connect } from 'react-redux';
import { toggleTodo } from '../actions';
import TodoList from '../components/TodoList';
// filterの値により絞り込みを行う
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
}
};
// StateをViewのプロパティに落とし込む
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
};
};
// ViewからStateにイベントを伝える
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
// ActionCreatorからActionを取得しStoreに渡す
dispatch(toggleTodo(id));
}
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
containers/AddTodo.jsにはViewが含まれていたが、containers/VisibleTodoList.jsではViewがcomponents/TodoList.jsやcomponents/Todo.jsで用意されているため、Reduxとのつなぎこみ部分だけを実装している。
- connect()の第1引数であるmapStateToProps関数は、Reduxが持つStateがViewに渡される際のデータを整形するための処理。
今回はStateのvisibilityFilterの値による絞り込みを行っている。 - connect()の第2引数であるmapDispatchToProps関数は、ReduxのStoreにがActionを渡す際のつなぎこみ。
今回はTODOをクリックした際に実行されるonTodoClickでtoggleTodoを使って生成したActionをStoreに渡している。
Reduxへの受け渡し処理をcontainersに書くことで、Viewはイベント処理をどこにどのように渡せばよいか知る必要がなくなり、純粋なViewとしての挙動のみを気にすればよい構成となる。
- containers/FilterLink.jsを作成(フィルタリング部分のつなぎこみ)
import { connect } from 'react-redux';
import {setVisibilityFilter, toggleTodo} from '../actions';
import Link from '../components/Link';
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter));
}
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Link);
- components/Footer.jsを作成(フィルタリング用リンクの表示)
import React from 'react'
import FilterLink from '../containers/FilterLink'
class Footer extends React.Component {
render() {
return (
<p>
Show: <FilterLink filter="SHOW_ALL">All</FilterLink>,
<FilterLink filter="SHOW_ACTIVE">Active</FilterLink>,
<FilterLink filter="SHOW_COMPLETED">Completed</FilterLink>
</p>
)
}
}
export default Footer;
FilterLinkでLinkとReduxのつなぎこみを行い、表示するためのFooterを作成している。
View周辺のエントリポイントとなるファイルを作成
- components/App.jsを作成
import React from 'react'
import Footer from './Footer';
import AddTodo from '../containers/AddTodo';
import VisibleTodoList from '../containers/VisibleTodoList';
class App extends React.Component {
render() {
return (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);
}
}
export default App;
作成した3つのコンポーネントが含まれている。
最後にエントリポイント(index.js)とindex.htmlを変更
- エントリポイント(index.js)を変更
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
const store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<div id="root">
</div>
<script src="./bundle3.js"></script>
Expressで実行
npm install --save express
- app.jsを作成
const express = require('express');
const app = express();
app.use(express.static('./'));
app.listen(3000, () => {
console.log('listening on post 3000');
});
node app.js
で実行
- http://localhost:3000で確認