React Redux入門

ReduxはJavaScriptのアプリケーションの状態を管理しやすくするためのフレームワークです。
FacebookがFluxというアーキテクチャを提唱していて、それをもとに作られたものがReduxだそうです。

Fluxとは

FluxはフロントエンドのWebアプリケーションをつくるためのアーキテクチャです。
データの流れを1方向としていて見通しのよいアプリケーションをつくることができます。
このアーキテクチャでは3つのコンポーネントが登場します。

  • Dispatcher 状態を更新するための命令(Action)をStoreに伝えます
  • Store アプリケーションの状態を一元的に管理します
  • View Reactコンポーネントです

Reduxとは

ReduxではStoreを管理するためのコンポーネントとしてReducerというものがあります。

  • Reducer ある状態とActionを受け取って、新しい状態を決定します

Counterアプリをつくる

つくったほうが理解が進みます。とりあえずつくってみましょう。

アプリケーションの作成とインストール

1
2
3
4
create-react-app react-counter
cd react-counter
npm install redux react-redux
npm start

コード

まずコードを載せます。
src/App.jsを以下のように変更して、http://localhost:3000 を開いてみてください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import React from 'react';
import { connect, Provider } from 'react-redux';
import { createStore } from 'redux';
import './App.css';

// Reducer
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};

// Counter component
const Counter = props => {
const count = props.count;
const increment = props.increment;
const decrement = props.decrement;
return (
<div>
Count: {count}
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};

const mapStateToProps = state => ({
count: state,
});

const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
};
};

const CounterConnect = connect(mapStateToProps, mapDispatchToProps)(Counter);

// App component
const store = createStore(counter);
const App = () => {
return (
<Provider store={store}>
<CounterConnect />
</Provider>
);
};

export default App;

IncrementボタンやDecrementボタンをクリックすることでCountの値が変わりましたか?
では、解説に移ります。

Reducer

Reducerは有限状態機械です。初期状態があり、受け取ったイベントによって別の状態へと遷移します。もちろん、遷移しないこともあります。
Reducerではイベントをアクションと呼ぶようです。

以下に今回のReducerを抜粋します。

1
2
3
4
5
6
7
8
9
10
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};

このReducerの状態は単なる数値です。引数は、初期状態とアクションです。
INCREMENTを受け取ったら状態の値に1加算し、DECREMENTを受け取ったら状態の値から1減算します。
戻り値は新しい状態の値です。

Counter component

続いてカウンターコンポーネントです。このコンポーネントは現在のカウント値、Incrementボタン、Decrementボタンを含みます。
しかし、現在のカウント値はStoreにあるため、何かしらの方法でStoreの値を参照する必要があります。
また、何かしらの方法でReducerに対してアクションを伝える必要があります。
このReactコンポーネントとStore、Reducerをつなぐためにconnectを使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Counter = props => {
const count = props.count;
const increment = props.increment;
const decrement = props.decrement;
return (
<div>
Count: {count}
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};

const mapStateToProps = state => ({
count: state,
});

const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
};
};

const CounterConnect = connect(mapStateToProps, mapDispatchToProps)(Counter);

connectの引数は2つあります。

mapStateToPropsは、どのStateの、どのプロパティをReactコンポーネントに渡すかを決めます。
今回Stateは1つしかなくプロパティもない値そのものなので、Stateをそのまま渡します。名前はcountです。
Counterコンポーネントでは{count}として表示しています。

mapDispatchToPropsは、どんなActionをReactコンポーネントに渡すかを決めます。
increment関数は{ type: 'INCREMENT' }をdispatchします。
CounterコンポーネントではonClickincrementが呼び出されるようにしています。

ボタンクリック時の流れ

では、Incrementボタンがクリックされたときの流れを説明します。

  1. IncrementボタンがクリックされるとonClickに定義されているincrement関数が呼び出されます。
  2. increment関数はconnect経由で渡ってきた関数です。これを呼び出すとcounterReducerが呼び出されます。
  3. case 'INCREMENT':の条件を満たし、stateの値が1増えます。
  4. stateの値が変化したのでCounterコンポーネントの再描画が行われます。
  5. その結果、画面上のcountの値が更新されます。

別の書き方

今までのコードの別の書き方を紹介します。
JavaScriptのいろいろな書き方をある程度知っておくと他のコードが読みやすくなります。

分割代入

1
2
3
const count = props.count;
const increment = props.increment;
const decrement = props.decrement;

これは、ES2015の分割代入を利用して以下のように書けます。

1
const { count, increment, decrement } = props;

この方法を使って、Counterの引数の部分を書き直すこともできます。

1
2
const Counter = props => {
const { count, increment, decrement } = props;

書き直すと以下のようになります。

1
const Counter = ({ count, increment, decrement }) => {

mapDispatchToProps

mapDispatchToPropsを別の書き方で書いてみます。

mapDispatchToPropsの2つ目の引数は、関数ではなくオブジェクトを渡すこともできます。このオブジェクトの要素は関数です。
オブジェクトを渡した場合、各要素の関数をdispatchでくるんでくれます。では、実際に書き直します。

1
2
3
4
5
6
const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
};
};

以下のようになります。

1
2
3
4
5
6
7
8
9
const increment = () => ({
type: 'INCREMENT',
});

const decrement = () => ({
type: 'DECREMENT',
});

const mapDispatchToProps = { increment, decrement };

コード量が増えてますが、incrementdecrementの関数はdispatchに依存しない純粋な関数になりました。