React 入門 - コンポーネントの作成と Redux の導入とルーティング



# はじめに

* 最近 Angular に入門した流れで React にも興味出てきたので入門してみた。
* React のコンポーネントの作り方、Redux の導入、react-router-redux によるルーティングの実装までやってみる

# 雛形を作成

* create-react-app というツールで雛形のアプリケーションを作成してくれる

```
npm install -g create-react-app
create-react-app my-react-app
cd my-react-app
```

# コンポーネントを作成

* 表示要素であるコンポーネントを作成する
* コンポーネントは react.Component を継承したクラスとして宣言し render メソッドで JSX によって記述された表示する仮想 DOM 要素を返却する
* JSX は XML ライクな JavaScript 拡張

```js:src/Component1.js
import React, { Component } from 'react';

class Component1 extends Component {
    render() {
        return (
            <div>
                Here is Component1.
            </div>
        )
    }
}

export default Component1
```

# 作成したコンポーネントを表示する

* App.js の App.render に Component1 を追加して表示する
* 作成したコンポーネントを import し render 内にコンポーネント名の要素を追加すると表示できる

```js:src/App.js
import Component1 from './Component1';

class App extends Component {
    render() {
        return (
            <div className="App">
                ...
                < Component1 />
            </div>
        );
    }
}
```

# コンポーネントに値を渡して表示する

* コンポーネント呼び出しもとで JSX 要素のプロパティに値を渡すと、コンポーネント側で参照することができる

## 呼び出し元で値を渡す

* Component1 要素に value1 プロパティを追加して "VALUE1" という文字列を渡す
* JSX 内で JavaScript を使う場合は `{ }` で囲む必要がある。`{"VALUE1"}` は JavaScript の文字列リテラルを渡しているという意味

```js:App.js
< Component1 value1={"VALUE1"}/>
```

## コンポーネントで値を参照する

* プロパティで渡された値は `this.props.{PROPERTY_NAME}` で参照することができる
* render 内に以下を追加することで渡された値を表示できる

```js:Component1.js
My props.value1 is {this.props.value1}.
```

# 関数でコンポーネントを定義する

* コンポーネントを定義する方法はいくつかあり、ここでは JavaScript の関数で定義してみる
* クラスとして定義する場合に比べて記述がシンプルになり実行効率がいいということで単純なコンポーネントなら関数として定義するのがよいということ
* コンストラクタを定義したり状態を持ったりしたい場合にはクラスとして定義しないとなので、どちらの書き方も覚えておいたほうがよさそう。

## コンポーネントの作成

```js:Component2.js
import React from 'react';

const Component2 = () => (
    <div>
        Here is Component2.
    </div>
);

export default Component2
```

## コンポーネントの表示

```js:App.js
import Component2 from './Component2';

class App extends Component {
    render() {
        return (
            <div className="App">
                ...
                <Component2 />
            </div>
        );
    }
}
```

# 関数で定義したコンポーネントのプロパティを参照する

* 関数で定義したコンポーネントでプロパティを参照したい場合は、関数の引数で受け取る
* プロパティを渡す側はクラスで定義した場合と同様

```js:Component2.js
const Component2 = (props) => (
    <div>
        Here is Component2. My props.value2 is {props.value2}.
    </div>
);
```

* プロパティのオブジェクトは[分割代入](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)を使って展開して渡すこともできる

```js:Component2.js
const Component2 = ({value2}) => (
    <div>
        Here is Component2. My props.value2 is {value2}.
    </div>
);
```

# Redux を導入する

## Redux とは

* Redux は状態管理をしてくれるライブラリ
* Redux はアプリの状態を一手に管理し React は状態の更新リクエストと描画をするだけでよくなる
* アプリケーションフレームワーク的に見れば React が View で Redux が Model とか ViewModel 
* 詳しくは[公式](https://github.com/reactjs/redux)とか[まとめ記事](http://qiita.com/kiita312/items/49a1f03445b19cf407b7)を読んだほうがいい

## インストール

```
npm install --save redux react-redux
```

## Store と Reducer を定義する

* Store は Redux 内でただひとつ存在するデータストア
* Store は状態を State という JavaScript オブジェクトに保持する
* Reducer は Store に付属するデータの更新機
* Reducer は Action という JavaScript オブジェクトを受け取って State の更新処理を行う

### Reducer の作成

* Reducer は純粋な JavaScript の関数で、現在の State と更新リクエストである Action を受け取って、新しい State を返却する
  * Action の実態は `{ type: "INCR" }` などのオブジェクト
* Reducer は State の階層構造とリンクして階層構造を持つことができる
  * State が `{ key1: 0, key2: [ 1, 2, 3 ] }` という構造の場合、State 全体を更新する Reducer をひとつだけ定義してもいいし、全体を更新する親 Reducer の下に `key1` や `key2` の更新を担当する子 Reducer を追加で定義してもいい
  * 子 Reducer の作成は下記の例のように `Redux.combineReducers` がファクトリーメソッドとして用意されている
  * `Redux.combineReducers` の引数のオブジェクトの構造が State の構造となる
  * State の初期値は Reducer の引数のデフォルト値として定義できる

```js:index.js
const countReducer = (state = 0, action) => {
    switch (action.type) {
        case "INCR":
            return state + 1;
        case "DECR":
            return state - 1;
        default:
            return state;
    }
};
const reducer = combineReducers({
    count: countReducer
});
```

* countReducer の変数名を count と変更すると State の要素名と同一になる、この場合は combineReducers の引数を以下のように省略できる

```js
const count = (state = 0, action) => {
    ...
};
const reducer = combineReducers({
    count
});
```

### Store の作成

* Store は `Redux.createStore` に Reduxer を渡して作成する

```js:index.js
const store = createStore(reducer);
```

### Store を React から参照できるようにする

* react-redex.Provider を JSX のルート要素として、作成した Store をプロパティで渡すことで子コンポーネントから参照する準備ができる

```js:index.js
ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>,
    document.getElementById('root')
);
```

## React のコンポーネントから State の参照と Action の投稿を行う

### State を参照する

* `react-redux.connect()()` 関数を使うと State からプロパティへの値のバインドが行われ State 更新に同期して表示も更新される
* `connect` の第一引数には State からプロパティに値を引き渡すためのポリシーとなる関数が渡され、第二引数にはコンポーネントが渡される
* `connect` に `mapStateToProps` という名前の関数を渡すとその名のとおり State から props へのマッピングが定義できる
* そのほかにも `mapDispatchToProps` や `mergeProps` など特定の名前の関数を定義してマッピングポリシーとして指定できる

```js:Counter.js
import React from 'react';
import {connect} from 'react-redux';

const Counter = (props) => (
    <div>
        <button onClick={() => props.dispatch({type: "INCR"})}>+</button>
        <button onClick={() => props.dispatch({type: "DECR"})}>-</button>
        <br />Counter: {props.count}
    </div>
)

function mapStateToProps(state) {
    return {count: state.count};
}

export default connect(mapStateToProps)(Counter)
```

### Action を投稿する

* Action の投稿はすでに前記のコードにあるとおり `props.dispatch` にアクションのオブジェクトを渡して実行する
* `props.dispatch` は `connect` 時にデフォルトで引き渡される
* `connect` の引数として `mapDispatchToProps` を使うと Action の送信処理を JSX の外で定義できる

```js:Counter.js
import React from 'react';
import {connect} from 'react-redux';

const Counter = (props) => (
    <div>
        <button onClick={props.onClickIncr}>+</button>
        <button onClick={props.onClickDecr}>-</button>
        Counter: {props.count}
    </div>
)

function mapStateToProps(state) {
    return {count: state.count};
}

function mapDispatchToProps(dispatch) {
    return {
        onClickIncr: () => dispatch({type: "INCR"}),
        onClickDecr: () => dispatch({type: "DECR"}),
    };
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter)
```

# この時点のコード

```js:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import {combineReducers, createStore} from "redux";
import {Provider} from 'react-redux'

const countReducer = (state = 0, action) => {
    switch (action.type) {
        case "INCR":
            return state + 1;
        case "DECR":
            return state - 1;
        default:
            return state;
    }
};
const reducer = combineReducers({
    count: countReducer
});

const store = createStore(reducer);

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>,
    document.getElementById('root')
);
registerServiceWorker();
```

```js:App.js
import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';

import Component1 from './Component1';
import Component2 from './Component2';
import Counter from './Counter';

class App extends Component {
    render() {
        return (
            <div className="App">
                <div className="App-header">
                    <img src={logo} className="App-logo" alt="logo"/>
                    <h2>Welcome to React</h2>
                </div>
                <p className="App-intro">
                    To get started, edit <code>src/App.js</code> and save to reload.
                </p>
                <Component1 value1={1 + 1} />
                <Component2 value2={"VALUE2"} />
                <Counter />
            </div>
        );
    }
}

export default App;
```

```js:Counter.js
import React from 'react';
import {connect} from 'react-redux';

const Counter = (props) => (
    <div>
        <button onClick={props.onClickIncr}>+</button>
        <button onClick={props.onClickDecr}>-</button>
        Counter: {props.count}
    </div>
)

function mapStateToProps(state) {
    return {count: state.count};
}

function mapDispatchToProps(dispatch) {
    return {
        onClickIncr: () => dispatch({type: "INCR"}),
        onClickDecr: () => dispatch({type: "DECR"}),
    };
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter)
```

# ルーターの導入

## 依存パッケージのインストール

```
npm install --save react-router react-router-redux history react-router-dom
```

## Reducer の追加

* ルーター用の Reducer を追加

```js:index.js
const reducer = combineReducers({
    count: countReducer,
    router: routerReducer
});
```

## Store にミドルウェアの追加

* history を追加してブラウザの戻る進むでコントロールできるように

```js:index.js
const history = createHistory()
const middleware = routerMiddleware(history)

const store = createStore(
    reducer,
    applyMiddleware(middleware)
);
```

## ルーティング設定の追加

```js:index.js
ReactDOM.render(
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <div>
                <Route exact path="/" component={App}/>
                <Route path="/counter" component={Counter}/>
            </div>
        </ConnectedRouter>
    </Provider>,
    document.getElementById('root')
);
```

## リンクの追加

* `react-router-dom.Link` でコンポーネントに他ページへのリンクを追加する
* App.js でも Counter.js を表示しているのでとりあえずこれでどのページでも画面遷移ができるようになる

```js:Counter.js
import {Link} from 'react-router-dom';

const Counter = (props) => (
    <div>
        <ul>
            <li><Link to={"/"}>Top</Link></li>
            <li><Link to={"/counter"}>Counter</Link></li>
        </ul>
        <button onClick={props.onClickIncr}>+</button>
        <button onClick={props.onClickDecr}>-</button>
        Counter: {props.count}
    </div>
);
```

## 全体のコード

* import を含めた全体のコードはこちら

```js:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import Counter from './Counter';
import registerServiceWorker from './registerServiceWorker';

import {combineReducers, createStore, applyMiddleware} from "redux";
import {Provider} from 'react-redux'

import createHistory from 'history/createBrowserHistory'
import {Route} from 'react-router'

import {ConnectedRouter, routerReducer, routerMiddleware} from 'react-router-redux'

const countReducer = (state = 0, action) => {
    switch (action.type) {
        case "INCR":
            return state + 1;
        case "DECR":
            return state - 1;
        default:
            return state;
    }
};
const reducer = combineReducers({
    count: countReducer,
    router: routerReducer
});

const history = createHistory()
const middleware = routerMiddleware(history)

const store = createStore(
    reducer,
    applyMiddleware(middleware)
);

ReactDOM.render(
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <div>
                <Route exact path="/" component={App}/>
                <Route path="/counter" component={Counter}/>
            </div>
        </ConnectedRouter>
    </Provider>,
    document.getElementById('root')
);
registerServiceWorker();
```