前回 QiitaにReact.js の React.js の公式Tutorial を gulp を利用して簡単に実行できる環境を作って、ES6も試した
で公式TutorialをECMA2015(ECMA6)を利用して書き直しを行ったのですが、ReactといえばFlux、Flux といえばRedux ということで、流行りのRedux を利用して記述したらどうなるのかを試してみました。
また 同じく流行りのテスト環境である Mocha + power-assert を利用して、テストを追加してみました。
React および Redux はテストも非常に行いやすいのを体感しました。
前回の続編として、以下GitHub で公開しています。
https://github.com/ma-tu/react-babel-gulp-browserify-tutorial/tree/es6-redux
React.js の version は 0.14.5 で Redux の version は 3.0.5 となります。
2016年1月時点のライブラリで作成しています。Windows 環境で確認しています。
Redux について
以下の和訳した情報が非常に読みやすくて参考になります。私がつたない文章で説明するよりも、**Redux入門【ダイジェスト版】10分で理解するReduxの基礎**を見ていただく方がわかりやすい。
Redux 対応のための変更ポイント
Action の作成
Redux では、状態を変更する操作については、Actionと呼ばれる、JavaScriptのオブジェクトを作成して、それを Store に連携することで実施するのが原則となります。今回のTutorialでいうと、以下の二つがActionとなります。
- コメントを追加する。
- サーバーからのコメント一覧取得を反映する。
以下の addComment , setComments が Action と呼ばれるJavaScriptのオブジェクトを作成する関数となります。Actionオブジェクトは必ず type という属性を持つ必要があります。
//src/actions/commentAction.js
import * as ActionTypes from '../constants/ActionTypes'
export function addComment(comment) {
return {
type: ActionTypes.ADD_COMMENT,
comment
};
}
export function setComments(comments) {
return {
type: ActionTypes.SET_COMMENTS,
comments
};
}
Reducer の作成
Reducer には Action を受けて、Store の State を変更するための手続きを記述することになります。
Actionオブジェクトには type属性が必ず存在するルールがあるので、それに従って分岐して記述していきます。
//src/reducers/commentReducer.js
import * as ActionTypes from '../constants/ActionTypes'
export default function commentReducer(state={comments : []}, action) {
switch(action.type){
case ActionTypes.ADD_COMMENT:
return {comments: state.comments.concat([action.comment])}
case ActionTypes.SET_COMMENTS:
return {comments: action.comments}
default:
return state;
}
}
Store の作成と React と Redux の接続
Store 作成および React と Redux の接続に関しては、基本的には記述がある程度決まっているので Redux の情報に従って記述していきます。
- Store を作成するための関数です。Redux の logger を利用して、ChromeのDevTool に Redux 変更時にログが出力されるようにしています。
//src/store/reduxStore.js
import {createStore, applyMiddleware} from 'redux';
import commentReducer from '../reducers/commentReducer'
import createLogger from 'redux-logger';
export default function configureStore() {
const logger = createLogger({logger:console});
const createStoreWithMiddleware = applyMiddleware(
logger
)(createStore);
const store = createStoreWithMiddleware(commentReducer);
return store;
}
- Redux と React をつなげる部分です。connect 関数の第一引数に React に対して Storeの情報を propsを利用して渡すための変換ロジックを記述します。第二引数は、ReactからStoreを変更するための関数をActionを利用して作成しているロジックです。
//src/containers/App.js
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import CommentBox from '../jsx/commentBox.jsx';
import * as commentActions from '../actions/commentAction';
function mapStateToProps(state) {
return {
url: '/api/comments',
pollInterval: 2000,
comments: state.comments
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(commentActions, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(CommentBox)
- store を作成して、Reactのアプリ(
<App>
)を react-redux の Provider にくるむ感じで記述します。
//src/jsx/example.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from '../store/reduxStore';
import App from '../containers/app';
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('content')
);
React の state を利用している部分を Redux を利用するように変更
- React のロジック中の stateを利用している部分および setState()を利用している部分を Redux を利用するように置き換えます。上記の
bindActionCreators(commentActions, dispatch)
のロジックにより commentActions の関数が React の props で利用できるようになっているのでそれを利用します。現在のStoreの値もmapStateToProps(state) {//}
で指定した形でprops 経由で利用可能となっています。
//src/jsx/commentBox.jsx の diff 抜粋
loadCommentsFromServer() {
@@ -19,36 +16,42 @@ export default class CommentBox extends React.Component{
url: this.props.url,
dataType: 'json',
cache: false,
- success: (data) => this.setState({data: data}),
+ success: (data) => {
+ this.props.setComments(data)
+ },
error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
});
}
handleCommentSubmit(comment) {
- let comments = this.state.data;
+ let comments = this.props.comments;
comment.id = Date.now();
let newComments = comments.concat([comment]);
- this.setState({data: newComments});
+ this.props.setComments(newComments)
対応完了版
以下のGitHubで公開しています。
https://github.com/ma-tu/react-babel-gulp-browserify-tutorial/tree/es6-redux
実行方法については 上記 GItHub の README.md で記述しています。
git clone https://github.com/ma-tu/react-babel-gulp-browserify-tutorial.git
cd react-babel-gulp-browserify-tutorial
git checkout es6-redux
npm install
gulp
Mocha + power-assert を利用したテストについて
Mocha + powerについては参考情報は以下を参考にしました。感謝
0からはじめるpower-assert
Action , Store , Reducer に関するテスト
Action , Store , Reducer は UI からも分離されているので、素直に実行した結果のJavaScriptオブジェクトを比較する形でテストは記述可能です。
//test/actions/commentActionTest.js
import assert from 'power-assert';
import * as commentActions from '../../src/actions/commentAction';
import * as ActionTypes from '../../src/constants/ActionTypes'
import * as commentUtil from '../testUtil/commentUtil'
describe('actions', () => {
it('addComment', () => {
const actual = commentActions.addComment(commentUtil.getPeteComment())
const expected = {type: ActionTypes.ADD_COMMENT, comment: commentUtil.getPeteComment()}
assert.deepEqual(actual, expected)
});
it('setComments', () => {
const actual = commentActions.setComments([commentUtil.getPeteComment()])
const expected = {type: ActionTypes.SET_COMMENTS, comments: [commentUtil.getPeteComment()]}
assert.deepEqual(actual, expected)
});
});
React の UI を含んだテスト
jsdom と呼ばれるライブラリを利用してテストをします。jsdom を利用すると仮想的なDOM環境をテストコードで利用できるのでブラウザを利用しない形で、Reactのテストを行うことができます。
- 以下のスクリプトで jsdom をテストで利用するための準備を行っています。この setup.js は mocha 実行時に呼び出されます。
//test/setup.js
import { jsdom } from 'jsdom'
global.document = jsdom('<!doctype html><html><body></body></html>')
global.window = document.defaultView
global.navigator = global.window.navigator
以下が React の UI を含んだテスト例となります。
-
setup()
で React で用意されている TestUtils を利用してテスト用のMockを呼び出すようにしています。また初期データとして Paul さんのコメントデータを追加しています。 -
TestUtils.findRenderedDOMComponentWithTag
関数でTagを利用してReactの仮想DOMを取得しています。 -
TestUtils.scryRenderedDOMComponentsWithTag
上記と同じですが、scry…は取得結果が複数件である場合に利用します。 -
container/App-display
のテストでは初期表示状態で Paul さんが表示されていること。またはコメントにはMarkdownの装飾が入っていることを確認しています。 -
container/append comment
のテストでは TestUtils.Simulate の機能を利用して、コメント欄に入力を行って、submitを実行して、その結果が期待通りであるかを確認しています。
//test/containers/AppTest.js
import assert from 'power-assert';
import React from 'react'
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import TestUtils from 'react-addons-test-utils'
import configureStore from '../../src/store/reduxStore';
import MockApp from '../testUtil/MockApp';
import CommentList from '../../src/jsx/commentList.jsx';
import * as commentActions from '../../src/actions/commentAction';
import * as commentUtil from '../testUtil/commentUtil'
function setup() {
const store = configureStore();
store.dispatch(commentActions.addComment(commentUtil.getPaulComment()))
const component = TestUtils.renderIntoDocument(
<Provider store={store}>
<MockApp />
</Provider>
)
return {
component: component,
h2: TestUtils.findRenderedDOMComponentWithTag(component, 'h2'),
span: TestUtils.findRenderedDOMComponentWithTag(component, 'span'),
inputs: TestUtils.scryRenderedDOMComponentsWithTag(component, 'input')
}
}
describe('container/App', () => {
it('display', () => {
const { h2, span } = setup()
assert.equal(h2.innerHTML, "Paul O’Shannessy")
assert.equal(span.innerHTML.trim(), "<p>React is <em>great</em>!</p>")
})
it('append comment', () => {
const { component, inputs } = setup()
TestUtils.Simulate.change(inputs[0], {target: {value: 'Test Author'}});
TestUtils.Simulate.change(inputs[1], {target: {value: 'Test **Text**'}});
TestUtils.Simulate.submit(inputs[2]);
const h2List = TestUtils.scryRenderedDOMComponentsWithTag(component, 'h2')
assert.deepEqual(h2List.map((item) => item.innerHTML), [ 'Paul O’Shannessy', 'Test Author' ])
const spanList = TestUtils.scryRenderedDOMComponentsWithTag(component, 'span')
assert.deepEqual(spanList.map((item) => item.innerHTML.trim()), [ '<p>React is <em>great</em>!</p>','<p>Test <strong>Text</strong></p>' ])
})
})
テスト実行
npm の package.json で mocha をコマンドで実行するようにしています。
npm run test
で実行できます。
//package.json
"scripts": {
"test": "mocha --compilers js:espower-babel/guess test/**/*.js --require ./test/setup.js"
},