前回 Redux入門 5日目 Reduxの基本・Data Flow
ReduxのアプリケーションはReact,Angular,Ember,jQuery, vanilla JavaScriptのいずれで作成することができます。
ReactはUIをstateの関数として扱うので特に相性がいいです。
ここからは、Reactを使ってtodoアプリに連携していきます。
Installing React Redux
ReactとReduxを連携するモジュールはReduxに含まれていないので、別途インストールする必要があります。
npm install --save react-redux
Container and Presentational Components
Reactのコンポーネントの設計として、一番上のレイヤーのコンポーネントはコンテナとしてReduxに関与し、下位のレイヤーのコンポーネントはReduxとは関与せずに、データはprop経由で受け取るようにするのをおすすめします。
今回のtodoアプリでは、コンテナとなるトップのコンポーネントを一つ用意します。もっと大きなアプリケーションになるとコンテナは複数持つことになります。
Designing Component Hierarchy
Reduxのstateツリーに合わせてViewの階層を設計していきます。
今回のtodoアプリの機能は、クリックするとtodoアイテムを完了にする、新規todoを作成するフィールドを表示する、todoすべて・完了のみ・未完了のみで表示を切り替えられる。
・ AddTodo:inputボタン
- onAddClick(text:string): ボタンがクリックされた時
・ TodoListは表示するtodoのリスト - todos: todoを入れるArray
- onTodoClick(index: number): todoがクリックされた時
・ Todo: todoアイテム - text: string 表示するテキスト
- completed: boolean 完了しているか
- onClick(): todoがクリックされた時
・ Footer: 表示を切り替えるためのコンポーネント - filter:string 現在有効になっているフィルター "SHOW_ALL","SHOW_COMPLETED","SHOW_ACTIVE"
- onFilterChange(nextFilter: string): フィルターを切り替えたとき
これらはPrestntational componentsなのでReduxには依存せず、データをどこから取得してどのように変更するかは意識しなくてもいい。
これらを実装して行きます。
Presentational Components
以下は一般的なReactのコンポーネントです。
import React, { Component, PropTypes } from 'react'
export default class AddTodo extends Component {
render() {
return (
<div>
<input type='text' ref='input' />
<button onClick={e => this.handleClick(e)}>
Add
</button>
</div>
)
}
handleClick(e) {
const node = this.refs.input
const text = node.value.trim()
this.props.onAddClick(text)
node.value = ''
}
}
AddTodo.propTypes = {
onAddClick: PropTypes.func.isRequired
}
import React, { Component, PropTypes } from 'react'
export default class Todo extends Component {
render() {
return (
<li
onClick={this.props.onClick}
style={{
textDecoration: this.props.completed ? 'line-through' : 'none',
cursor: this.props.completed ? 'default' : 'pointer'
}}>
{this.props.text}
</li>
)
}
}
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
}
import React, { Component, PropTypes } from 'react'
import Todo from './Todo'
export default class TodoList extends Component {
render() {
return (
<ul>
{this.props.todos.map((todo, index) =>
<Todo {...todo}
key={index}
onClick={() => this.props.onTodoClick(index)} />
)}
</ul>
)
}
}
TodoList.propTypes = {
onTodoClick: PropTypes.func.isRequired,
todos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
}).isRequired).isRequired
}
import React, { Component, PropTypes } from 'react'
export default class Footer extends Component {
renderFilter(filter, name) {
if (filter === this.props.filter) {
return name
}
return (
<a href='#' onClick={e => {
e.preventDefault()
this.props.onFilterChange(filter)
}}>
{name}
</a>
)
}
render() {
return (
<p>
Show:
{' '}
{this.renderFilter('SHOW_ALL', 'All')}
{', '}
{this.renderFilter('SHOW_COMPLETED', 'Completed')}
{', '}
{this.renderFilter('SHOW_ACTIVE', 'Active')}
.
</p>
)
}
}
Footer.propTypes = {
onFilterChange: PropTypes.func.isRequired,
filter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
}
ダミーAppを使ってviewの描画を行ないます。
import React, { Component } from 'react'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'
export default class App extends Component {
render() {
return (
<div>
<AddTodo
onAddClick={text =>
console.log('add todo', text)
} />
<TodoList
todos={
[
{
text: 'Use Redux',
completed: true
},
{
text: 'Learn to connect it to React',
completed: false
}
]
}
onTodoClick={index =>
console.log('todo clicked', index)
} />
<Footer
filter='SHOW_ALL'
onFilterChange={filter =>
console.log('filter change', filter)
} />
</div>
)
}
}
Connecting to Redux
Reduxと連結していきます。
AppコンポーネントをReduxに変更して、actionのdispatchとReduxのストアからstateを読み込めるようにします。
まず、react-reduxが提供しているProviderを使用して一番親のコンポーネントをラップします。
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import todoApp from './reducers'
let store = createStore(todoApp)
let rootElement = document.getElementById('root')
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
これにより、App内でstoreのインスタンスが使用できるようになりました。
次にreact-reduxが提供しているconnect()メソッドを使用して、Reduxと連携させるコンポーネントをラップします。データフローの追跡がしやすいように、上部のレイヤーのコンポーネントだけ、連携させるようにしましょう。
connect()でラップされたコンポーネントはdispatchメソッドとstateをpropで渡される。
connect()の一つ目の引数のメソッドはselectorと呼ばれており、このメソッドではstoreからstateを受け取り、コンポーネントが必要なpropの形に合わせて返すようにします。
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'
class App extends Component {
render() {
// Injected by connect() call:
const { dispatch, visibleTodos, visibilityFilter } = this.props
return (
<div>
<AddTodo
onAddClick={text =>
dispatch(addTodo(text))
} />
<TodoList
todos={visibleTodos}
onTodoClick={index =>
dispatch(completeTodo(index))
} />
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
} />
</div>
)
}
}
App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
}
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed)
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed)
}
}
// Which props do we want to inject, given the global state?
// Note: use https://github.com/faassen/reselect for better performance.
function select(state) {
return {
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
}
}
// Wrap the component to inject dispatch and state into it
export default connect(select)(App)
これで連携完了です。