React FluxアプリをReact Redux(ES2015)で書き直した時のメモ

しばらくReact周りの情報を追うのが疎かになっていた感があったので、
ReactアプリをES2015 classでリファクタリングして、ついでにReduxにのせてみた際に学んだことの個人的なまとめ。

すでに自明のことばかりであるけど。

ES2015 classを使う

class

例えばES5でコンポーネントクラスを作成するcreateClassを使用している。

1
2
3
4
5
// ES5 style
var Task = React.createClass({
handleClick: function(e) { ... },
render: function() { ... },
});

React v0.13からはES2015のclassへのサポートされてるので、
ES2015 class定義を使って書くときは、createClassでなく、Componentを使うことで可能になる。

1
2
3
4
5
// ES2015 class style
class Task extends React.Component {
handleClick() {...}
render() {...}
}

functionが消えよりシンプルになる。ただしこれだけではまだエラーとなる。

State, Property

コンポーネントのインスタンス作成時にstateを初期化するために、getInitialState。 propertyのdefault値を定義するgetDefaultPropsやpropsの情報を保持するpropTypesなど一般的にコンポーネントには必要な情報がある。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ES5 style
var Task = React.createClass({
getInitialState: function() {
  return { complete: this.props.todo.complete }
}),
getDefaultProps: function() {
return { complete: false }
},
propTypes: {
complete: React.PropTypes.bool.isRequired
},
render: function() { ... },
});

ES2015 classで記述する場合は、getInitialStateを使わずに、constructorで行う。
これでより意図が通じる形に書きなおすことができる。
また、static修飾子を使いpropsの設定値を静的メンバとして登録するので、getterの定義が不要となる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ES2015 class style
class Task extends React.Component {
constructor(props) {
super(props);
this.state = { complete: this.props.todo.complete };
}

static defaultProps = {
complete: false
}

static propTypes = {
complete: React.PropTypes.bool.isRequired
}

render() {...}
}

Instance Method

コンポーネントのイベントなどで呼ばれるmethod群は、createClassを用いる場合はthisが自動でコンポーネントにバンドされる(Autobinding)。
New in React v0.4: Autobind by Default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ES5
var Task = React.createClass({
getInitialState: function() {
  return { complete: this.props.todo.complete }
}),
getDefaultProps: function() {
return { complete: false }
},
propTypes: {
complete: React.PropTypes.bool.isRequired
},
handleClick: function(event) { ... },
render: function() { ... },
});

ES2015 class を用いる場合このAutobindingが行われないため、
constructorの中で手動でbindを使ってthisの束縛を明示的に行うことが必要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ES2015 class style
class Task extends React.Component {
constructor(props) {
super(props);
this.state = { complete: this.props.todo.complete };
this.handleClick = this.handleClick.bind(this);
}

static defaultProps = {
complete: false
}

static propTypes = {
complete: React.PropTypes.bool.isRequired
}

handleClick(event) { ... }

render() {...}
}

class定義を使う場合、幾つかのがメソッド(isMountedなど)利用できなくなる。

実際、class定義、createClassのどちらを使うのかとかの議論している記事もあり、
まだどちらがスタンダードというわけでもなさそう。

個人的にはcreateClassを使いcomponentのclassを作成するよりも、class定義を使う方が将来性もあるので良い気がする。

Reduxを使う

Fluxなどの思想をもとに作成されたJavaScripアプリの状態管理のライブラリ。
1つのStoreオブジェクトでstateは管理され、データフローはFluxと同じく一方向。
アプリケーション全体の状態を管理しているstateの変更はactionオブジェクトから渡されるデータをもとに、reducerを通してのみ行われる。

Reduxを利用するにあたり、documentを参考に登場人物たちを一旦整理する。

ちなみにFluxのdata flowは

actions

plainなオブジェクトであり、stateの状態を変更する情報(type)を持つ必要がある。

reducers

reducersはシンプルな関数である(Changes are made with pure functions)。
actionから送られたtypeをもとに、実際にstateを変更する。

states

アプリケーション全体の状態を管理するシングルオブジェクト。
stateはImuutable(State is read-only)である必要があるので、stateを直接変更しないで、Object.assign()などを使って必ずコピーする。

stores

applicationの状態(state)を管理し、actionsとreducersを統合しているオブジェクト。
rootReducerにより初期状態のstoreが生成される。
また、getState()でstoreにアクセスできる。dispatch(action)でstateを更新でき、subscribe(listener)でListner登録できる。

container

Reduxの最上位のレイヤーであり、ここでReduxのstateとcomponentをつなぐ役割を果たす。

components

Reduxとは関連しないView。
storeのdataをcontainerよりpropsのバケツリレーで受け取る。

Reduxのデータフローは

移行にあたり変わる箇所

ActionCreatorはReduxと違いシンプルな関数であり、FluxではComponentで呼ばれているが、
ReduxではContainerでDispatchされるようになる。

1
2
3
4
5
// flux action
var TodoActions = {
create: function(text) { ... },
update: function(text) { ... },
}
1
2
3
// redux action
export function create(text) { ... },
export function update(text) { ... },

また、FluxではStoreのメソッドでstateを変更するが、ReduxではReducerでのみStoreの値を変更する。
ReduxではStoreにのみ状態が保持される形となる。

1
2
3
4
5
6
7
8
9
10
// redux reducer で stateを変更する
export default function todos(state, action) {
switch(action.actionType) {
case 'CREATE':
// create state
case 'UPDATE':
// update state
...
}
}

データの取得に関しても、Fluxでは必要に応じてComponentが直接Storeから値を取得することができたが、
Reduxではデータはcontainerで取得され、componentにはpropsとしで渡されてる。

Fluxでは複数のStoreにデータを保持するが、Reduxでは1つのStoreでデータを管理するため、複数のStoreはツリー構造で管理するようになる。

Build Process

browserify

webpackでもいいけれど、個人的にモノシリックに感じるのでbrowserifyを利用。
ES2015 classのinstance filedとstaticの変換はStage 1 presettransform-class-propertiesを使うようになる。

所感

Fluxでは処理の中心にはdispacherが存在し、そこから渡されるactionを通して変更が各コンポーネントに通知され、
UIが更新される。

これに対してReduxの場合は中心となるのはシングルオブジェクトのstate(single source of truth)。stateの変更を受けUIが変更される。
stateが1つのオブジェクトとして構成されるので、その管理がシンプルになりそう。
また、fluxのstoreでのcallback周りの管理をreducerと切り出しているので、そういう点でもシンプルに
管理できるように感じる点だった。

ReduxはFRP(Functional Reactive Programming)的なアプローチとは反対に、保持している状態(state)を安全に上手に遷移させていくということに焦点が絞られている。

学ぶにあたり作ったtodoアプリ kazu69/example-react-redux

参考にしたページ

https://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
https://github.com/yannickcr/eslint-plugin-react/issues/203
http://babeljs.io/docs/plugins/preset-stage-1/
http://rackt.org/redux/index.html
https://github.com/rackt/redux/issues/756
http://staltz.com/unidirectional-user-interface-architectures.html

Comments