React + Redux の簡単なサンプル


個人向けのReactでRedux使うメモ。

>個人的にはReactNativeを使うことが多く、そちらで使ってる書き方がそのまま使えるかテスト。

## 前提

ページ移動が無いとReduxの意味が無いので（薄れ得るので）react-router-domでページを移動させてみます。

* create-react-app使います
* ルーティングにreact-router-dom使います

## 作るもの

state（変数）とaction（関数）の共有を確認するため下記のようなものを作りたいと思います。
各ページにボタンを配置し、そこで変数(name,age)を更新、それぞれが他ページでも反映（共有）されているかを確認します。

![スクリーンショット 2019-12-12 7.28.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/12846dbe-ce77-3780-f9f3-28876c54efa8.png "スクリーンショット 2019-12-12 7.28.45.png")


## 準備


### 場所とファイルの準備

作業場所を確保して移動しておきます。

```bash
create-react-app redux-test
cd redux-test
```

### モジュールのインストール


必要なモジュールをインストールします。

```
yarn add react react-redux react-router-dom
```

### ファイルの準備

既存に加え、必要なファイルを追加しておきます。

```bash
cd src
mkdir screens reducers actions

touch screens/Home.j screens/About.js
touch reducers/userReducer.js
touch actions/userAction.js
```

## 実装：基本

### はじめに

Reduxを利用したアプリは実装の説明は難しいです。。。
以下、「ファイルが無い」というエラーがでない順番で紹介していますが、先に各ファイルで何をしているのか確認してイメージした上で進めたほうがいいかもしれません。

1. App.jsでルーティング設定

### userAction.js

ここではnameとageをアップデートするためのaction(関数）を実装しています。

```js:userAction.js
export const UPDATE_NAME = 'UPDATE_NAME';
export const UPDATE_AGE = 'UPDATE_AGE';

export const updateName = name => {
    return {
        type: UPDATE_NAME,
        payload: name
    }
}

export const updateAge = age => {
    return {
        type: UPDATE_AGE,
        payload: age
    }
}
```

### userReducer.js

ここでは各アクションが呼び出されたとき、stateをどう変更するかを定義しています。

```js:userReducer.js
import { UPDATE_NAME, UPDATE_AGE } from '../actions/userAction';

const initialState = {
    name: 'hoge',
    age: 33
}

const user = (state = initialState, action) => {
    switch (action.type) {
        case UPDATE_NAME:
            let nameState = {...state};
            nameState.name = action.payload;
            return nameState;
        case UPDATE_AGE:
            let ageState = {...state};
            ageState.age = action.payload;
            return ageState;
        default:
            return state;
    }
}

export default user;
```




### index.js

アプリの起点となるindex.jsにて、アプリ全体でstate等を共有するための箱(store)を作成し、全体を\<Provider>\</Provider>タグで囲っています。

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

//redux
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import user from './reducers/userReducer';

//store
const store = createStore(combineReducers({
    user: user,
}), applyMiddleware())

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

### Home.js

App.jsの中でimportするので先に作成しておきます。

```jsx:Home.js
import React from 'react';
import { Link } from 'react-router-dom';

//redux
import { connect } from 'react-redux';
import { updateName, updateAge } from '../actions/userAction';

class Home extends React.Component {

    handleOnClick = () => {
        this.props.updateName("foo@Home");
    }

    render() {
        console.log(this.props.user);
        return (
            <div>
                <p>Home</p>
                <Link to="/about">Aboutへ</Link>
                <p>名前:{this.props.user.name}</p>
                <p>年齢:{this.props.user.age}</p>
                <button onClick={this.handleOnClick}>updateName@Home</button>
            </div>
        );
    }
}

const mapStateToProps = state => (
    {
        user: state.user,
    }
)

const mapDispatchToProps = dispatch => (
    {
        updateName: name => dispatch(updateName(name)),
        updateAge: age => dispatch(updateAge(age)),
    }
)

export default connect(mapStateToProps, mapDispatchToProps)(Home);

```

### About.js

App.jsの中でimportするので先に作成しておきます。

```jsx:About.js
import React from 'react';
import { Link } from 'react-router-dom';

//redux
import { connect } from 'react-redux';
import { updateName, updateAge } from '../actions/userAction';

class About extends React.Component {

    handleOnClick = () => {
        this.props.updateAge(44);
    }

    render() {
        return (
            <div>
                <p>About</p>
                <Link to="/">Homeへ</Link>
                <p>名前:{this.props.user.name}</p>
                <p>年齢:{this.props.user.age}</p>
                <button onClick={this.handleOnClick}>updateAge@About</button>
            </div>
        );
    }
}

const mapStateToProps = state => (
    {
        user: state.user,
    }
)

const mapDispatchToProps = dispatch => (
    {
        updateName: name => dispatch(updateName(name)),
        updateAge: age => dispatch(updateAge(age)),
    }
)

export default connect(mapStateToProps, mapDispatchToProps)(About);
```


### App.js


各コンポーネントをimportしルーティングを設定します。


```jsx:App.js
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';

//screens
import Home from './screens/Home';
import About from './screens/About';

class App extends React.Component {
    render() {
        return (
            <BrowserRouter>
                <Switch>
                    <Route exact path="/" component={Home} />
                    <Route path="/about" component={About} />
                    <Route render={() => <p>Page not found.</p>} />
                </Switch>
            </BrowserRouter>
        );
    }
}

export default App;
```


###　動作確認

動作確認します（随時したほうがいいでしょう）。

```bash
yarn start
```


## 実装：応用

実務ではAPI連携等の非同期処理を起点としてstateを更新したりすることがよくあります。
その際に利用するしくみとしてthunkがあります。その使い方も軽くメモしておきます。

### モジュールのインストール

thunkと非同期通信用のaxiosをインストールしておきます。

```bash
yarn add redux-thunk axios
```

### APIの準備

今回は下記のような値が返るテスト用のAPIを個人的に作っていたのでそれをそのまま利用してみます。

```json
{"status":"OK","message":"Hello2019-12-12 07:21:17"}
```

### index.js


index.jsのcreateStore()のところでthunkを読み込むようにします。

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

//redux
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import user from './reducers/userReducer';
+import thunk from 'redux-thunk';

//store
const store = createStore(combineReducers({
    user: user,
}), applyMiddleware(
+    thunk,
))

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

### userAction.js


userAction.jsを下記のようにします。
値更新用のactionを新たに作らず、APIからの戻り値をdipatchしてnameUpdate()を呼び出すようにしています。

```diff:userAction.js
+import axios from 'axios';

export const UPDATE_NAME = 'UPDATE_NAME';
export const UPDATE_AGE = 'UPDATE_AGE';

export const updateName = name => {
    return {
        type: UPDATE_NAME,
        payload: name
    }
}

export const updateAge = age => {
    return {
        type: UPDATE_AGE,
        payload: age
    }
}

+//ここ以下を追加
+//axiosで外部APIをたたき、そのレスポンスをdispatchによりuser.nameに入れている
export const getApiData = () => {
    return async (dispatch, getState) => {
        try {
            const url = "http://www.bluecode.jp/test/api.php";
            const res = await axios.get(url)
            const json = res.data;
            dispatch({
                type: UPDATE_NAME,
                payload: json.message
            });
        } catch (e) {
            alert(e);
        }
    }
}
```

### Home.js

作成したactionを利用できるようにします。

```diff:Home.js
import React from 'react';
import { Link } from 'react-router-dom';

//redux
import { connect } from 'react-redux';
+import { updateName, updateAge, getApiData } from '../actions/userAction';

class Home extends React.Component {

    handleOnClick = () => {
        this.props.updateName("foo@Home");
    }

+    handleGetApi = () => {
+        this.props.getApiData();
+    }

    render() {
        return (
            <div>
                <p>Home</p>
                <Link to="/about">Aboutへ</Link>
                <p>名前:{this.props.user.name}</p>
                <p>年齢:{this.props.user.age}</p>
                <button onClick={this.handleOnClick}>updateName@Home</button>
+                <button onClick={this.handleGetApi}>get data from Api</button>
            </div>
        );
    }
}

const mapStateToProps = state => (
    {
        user: state.user,
    }
)

const mapDispatchToProps = dispatch => (
    {
        updateName: name => dispatch(updateName(name)),
        updateAge: age => dispatch(updateAge(age)),
+        getApiData: () => dispatch(getApiData()),
    }
)

export default connect(mapStateToProps, mapDispatchToProps)(Home);
```

### 動作確認

ボタンをクリックするとAPIから値が取得されname値として反映されます。


![スクリーンショット 2019-12-12 7.21.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/7d02389f-befc-2d8f-88eb-e6b3500b27ac.png "スクリーンショット 2019-12-12 7.21.01.png")

簡単ですが以上です。