Reduxの書き方手順

どこかのReduxのチュートリアルをやった時に書いたメモをまとめました。
この手順通りに書いていくのが自分にとって一番わかりやすかったです。

# インストール

## Redux
```
npm install --save redux react-redux
```
## Router
```
npm install --save react-router-dom react-router-redux@next history
```
## Midleware
```
npm install --save redux-logger redux-thunk
```
## PropTypes
```
npm install --save prop-types
```

# 書く手順

## Reducer

```react:src/reducers/tasks.js

const initialState = {
  task: '',
  tasks: []
};

export default function tasksReducer(state = initialState, action) {
  switch (action.type) {
    case 'INPUT_TASK':
      return {
        ...state,
        task: action.payload.task
      };
    case 'ADD_TASK':
      return {
        ...state,
        tasks: state.tasks.concat([action.payload.task])
      };
    default:
      return state;
  }
}
```
- `initialState`でStoreに保存するStateを決める。
- Reducerが多く、分割する場合は`combineReducer`を用いる。(createStore.jsでまとまる場合は不要(?))

```react

import { combineReducers } from 'redux';

const rootReducer = combineReducers({
  messageModal,
  aService,
  errorMessage,
  router
});

export default rootReducer;
```

## Store

```react:src/createStore.js

import {
  createStore as reduxCreateStore,
  combineReducers,
  applyMiddleware
} from 'redux'
import {routerReducer, routerMiddleware} from 'react-router-redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import tasksReducer from '../reducers/tasks'

export default function createStore(history) {
  return reduxCreateStore(
    combineReducers({
      tasks: tasksReducer,
      router: routerReducer,
    }),
    applyMiddleware(
      routerMiddleware(history),
      logger,
      thunk
    )
  );
}
```
- ReducerとMiddlewarをここでStoreに紐づける。
- `combineReducer`では分割された子Reducer名と同じキーの`state`が使用できる。(ex. state.tasks, state.router)
- [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ja)を用いる場合は以下。

```react:src/createStore.js
import {
  createStore as reduxCreateStore,
  combineReducers,
  applyMiddleware,
  compose
} from 'redux'
import {routerReducer, routerMiddleware} from 'react-router-redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import userReducer from "./reducers/userReducer";
import countReducer from "./reducers/countReducer";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

export default function createStore(history) {
  return reduxCreateStore(
    combineReducers({
      user: userReducer,
      count: countReducer,
      router: routerReducer,
    }),
    composeEnhancers(
      applyMiddleware(
        routerMiddleware(history),
        logger,
        thunk
      )
    )
  );
}
```

## Root

```react:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {Route} from 'react-router-dom'
import {Provider} from 'react-redux'
import {ConnectedRouter} from 'react-router-redux'
import createBrowserHistory from 'history/createBrowserHistory'
import createStore from './store/index'
import TodoApp from './containers/TodoApp'
import Error from './components/Error'

const history = createBrowserHistory();
const store = createStore(history);

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <div>
        <Route exact path="/" component={TodoApp}/>
        <Route exact path="/error" component={Error}/>
      </div>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
);
```
- `Provider`でComponentに`store`を紐づける。
- `ConnectedRouter`でComponentに`history`を紐づける。
- `Route`でRoutingを行う。
- ここにはないが、`react-router-dom`の`Link`を用いてページ遷移する。

```react
import {Link} from 'react-router-dom'
<Link to="/error">error</Link>
```
- または、`react-router-dom`の`Switch`と、

```react
import {Route, Switch} from 'react-router-dom'

<Switch>
  <Route exact path="/" component={TodoApp}/>
  <Route exact path="/error" component={Error}/>
<Switch/>
```
- `react-router-redux`の`push`を用いてページ遷移する。

```react
import {push} from 'react-router-redux'

dispatch(push('/error'))
```


## ActionCreater

```js:src/actions/taks.js
export const inputTask = (task) => ({
  type: 'INPUT_TASK',
  payload: {
    task
  }
});

export const addTask = (task) => ({
  type: 'ADD_TASK',
  payload: {
    task
  }
});
```
### flux-standard-action規約
```react
{
  type: FOO_TYPE,      // must
  payload: {object},   // optional
  meta: {object},      // optional
  error: false, true, undefined, null, ... // optional
}
```
### `thunk`を用いた非同期なActionCreater
```js
export const signIn = (name, email, password) => ({
  type: 'SIGN_IN',
  payload: {name, email, password}
});

export const asyncSignIn = (email, password) => {
  return dispatch => {
    request
      .get(url)
      .set('Content-Type', 'application/json')
      .set('Access-Control-Allow-Origin', '*')
      .query({email: email, password: password})
      .end(function (err, res) {
        if (err) {
          console.log(err);
        } else {
          console.log(res);
          dispatch(signIn(res.body.name, res.body.email, res.body.password));
          dispatch(push('/count'));
        }
      });
  };
};
```
### `thunk`を用いた複数のActionCreaterをまとめたActionCreater
```react

export const inputEmail = (email) => ({
  type: 'INPUT_EMAIL',
  payload: {email}
});

export const inputPassword = (password) => ({
  type: 'INPUT_PASSWORD',
  payload: {password}
});

export const inputAction = (email, password) => {
  return (dispatch) => {
    dispatch(inputEmail(email));
    dispatch(inputPassword(password));
  };
};
```

## Container

```react:src/containers/Ranking.js
import {connect} from 'react-redux'
import {push} from 'react-router-redux'
import TodoApp from '../components/TodoApp'
import {inputTask, addTask} from "../actions/tasks";

const mapStateToProps = ({tasks}) => {
  return {
    task: tasks.task,
    tasks: tasks.tasks
  };
};

const mapDispatchToProps = (dispatch) => ({
  addTask: task => dispatch(addTask(task)),
  inputTask: task => dispatch(inputTask(task)),
  redirectToError: () => dispatch(push('/error'))
});

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
```
- `mapStateToProps`で`state`を受け取り、Componentに渡すものを決める。
- `state.hoge`などは`hoge`で扱える。
- `mapDispatchToProps`で`dispatch`を受け取り、importしたActionCreaterを用いてComponentに渡すメソッドを決める。
- `connect`でComponentに`mapStateToProps`と`mapDispatchToProps`を渡す。

## Component

```react:src/component/TodoApp.js
import React from 'react'
import PropTypes from 'prop-types'

const TodoApp = ({task, tasks, inputTask, addTask, redirectToError}) => (
  <div>
    <input type="text" onChange={(e) => inputTask(e.target.value)}/>
    <input type="button" value="add" onClick={() => addTask(task)}/>
    <ul>
      {
        tasks.map((item, i) => {
          return (
            <li key={i}>{item}</li>
          );
        })
      }
    </ul>
    <button onClick={() => redirectToError()}>エラーページへ</button>
  </div>
)

TodoApp.propTypes = {
  task: PropTypes.string.isRequired,
  tasks: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
  inputTask: PropTypes.func.isRequired,
  addTask:  PropTypes.func.isRequired,
  redirectToError: PropTypes.func.isRequired
}

export default TodoApp
```
- Containerから渡された`mapStateToProps`, `mapDispatchToProps`の中身を引数に取る。
- `PropTypes`を用いて`propTypes`を設定する。