windows + vagrant + virtualbox + ubuntu16.04 + docker で s2sのredux-sagaのサンプルを試してみたメモ


[s2sをdockerで試してみたメモ](https://qiita.com/hibohiboo/items/14433aa1061bcf472a21#_reference-07849505c4b70a60e147), [docker + webpack-dev-server で s2sのshopping-cartのサンプルを試してみたメモ](https://qiita.com/hibohiboo/items/7d83a2583dbf8b145c7e
)に引き続き、サンプルを動かしてみる。

今回は[s2s-redux-actions-sample](https://github.com/cndlhvn/s2s-redux-actions-sample)を使用。

## 動作環境

* windows10
* vagrant1.9.7
* virtualbox5.1.26
* ubuntu-16.04
* Docker version 17.09.0-ce, build afdb6d4
* docker-compose version 1.17.1, build 6d101fb

## フォルダ構成

```yaml
app
  - docker
    - s2s
      - Dockerfile
    - docker-compose.yml
  + src
  + dist
```

[最終的なソース](https://github.com/hibohiboo/develop/tree/e2acf11dcda90868e948b38dea30e274288c0348/tutorial/lesson/s2s-saga)

## docker設定

```docker:docker/s2s/Dockerfile
FROM node:9.2.0

# コンテナ上の作業ディレクトリ作成
WORKDIR /app

# ソースダウンロード
RUN git clone --depth=1 https://github.com/cndlhvn/s2s-redux-actions-sample.git

# s2sをビルド
WORKDIR /app/s2s-redux-actions-sample
RUN yarn

# chokidarのポーリング設定
RUN sed -i -e "s/\(ignoreInitial: true\)/\1,\n    usePolling: true, /g" /app/s2s-redux-actions-sample/node_modules/s2s/lib/index.js
```

```yaml:docker/docker-compose.yml
version: '3'
services:
  s2s:
    build: ./s2s
    volumes:
      - ../src:/app/s2s-redux-actions-sample/src
      - ../dist/build:/app/s2s-redux-actions-sample/build
    command: [yarn, run, s2s]

  dev-server:
    build: ./s2s
    volumes:
      - ../src:/app/s2s-redux-actions-sample/src
    ports:
      -  8080:3000
    command: [yarn, start]
```

## 起動

`cd docker && docker-compose up` で起動。

![2017-11-25 (1).png](https://qiita-image-store.s3.amazonaws.com/0/99876/5dad4d02-7695-1ea7-8ebb-0e2082968257.png)

## Actionの作成

### 新規作成

`touch src/actions/coin.js`でファイルを新規作成。

![2017-11-25 (2).png](https://qiita-image-store.s3.amazonaws.com/0/99876/ef5606b9-d7d4-2454-6034-b173b8cf3dad.png)

coin.jsにデフォルト値が設定され、index.jsが追加される。

```js:src/actions/coin.js
import { createAction } from 'redux-actions'
```

```js:src/actions/index.js(自動生成)
export * from "./coin";
```

[この時点のソース](https://github.com/hibohiboo/develop/tree/eaceda2ab524d15386fb1b1aa8656a5a5a1e6d96/tutorial/lesson/s2s-saga)


### actionの追加

つぎに、`coin.js`に`getCoinRequest`と書いて保存。

```js:src/actions/coin.js（保存前）
import { createAction } from 'redux-actions'
getCoinRequest
```

```js:src/actions/coin.js(保存後)
import { createAction } from "redux-actions";

export const getCoinRequest = createAction("GET_COIN_REQUEST");
export const getCoinSuccess = createAction("GET_COIN_SUCCESS");
export const getCoinFailure = createAction("GET_COIN_FAILURE");
```

## Reducerの作成

### 新規作成

`touch src/reducers/coin.js`でファイルを新規作成。

![2017-11-25 (4).png](https://qiita-image-store.s3.amazonaws.com/0/99876/7e53155d-9615-e0f9-14ec-6a74c4f905a4.png)

```js:src/reducers/coin.js
import { handleActions } from 'redux-actions'
import * as actions from '../actions'

const initialState = {}

export default handleActions(
  {},
  initialState
)
```

```js:src/reducers/index.js(自動生成)
import { combineReducers } from "redux";
import { routerReducer } from "react-router-redux";
import coin from "./coin";
export default combineReducers({
  coin,
  routing: routerReducer
});
```

### actionの追加

つぎに、`export default handleActions(  {},  initialState)`の`{}`に`getCoinRequest`と書いて保存。

```js:src/reducers/coin.js（保存前）
import { handleActions } from 'redux-actions'
import * as actions from '../actions'

const initialState = {}

export default handleActions(
  {getCoinRequest},
  initialState
)
```

```js:src/reducers/coin.js（保存後）
import { handleActions } from "redux-actions";
import * as actions from "../actions";

const initialState = {};

export default handleActions(
  {
    [actions.getCoinRequest]: (state, action) => ({
      ...state
    }),
    [actions.getCoinSuccess]: (state, action) => ({
      ...state
    }),
    [actions.getCoinFailure]: (state, action) => ({
      ...state
    })
  },
  initialState
);
```

![2017-11-25 (5).png](https://qiita-image-store.s3.amazonaws.com/0/99876/52904735-d9da-d20a-2380-c8ce1832a047.png)

### 手作業

initialStateについては手動で設定。

```js
const initialState = {
  coin: {
    id: "",
    name: "",
    symbol: "",
    rank: "",
    price_usd: "",
    price_btc: "",
    "24h_volume_usd": "",
    market_cap_usd: "",
    available_supply: "",
    total_supply: "",
    max_supply: "",
    percent_change_1h: "",
    percent_change_24h: "",
    percent_change_7d: "",
    last_updated: ""
  }
};
```

## Sagaの作成

### 新規作成

同様に、`touch src/sagas/coin.js`で作成。

```js:src/sagas/coin.js
import { put, call,takeLatest } from 'redux-saga/effects';
import * as actions from '../actions';
import api from '../api';

export default [];
```

```js:src/sagas/index.js(自動生成)
import { all } from "redux-saga/effects";
import coin from "./coin";
export default function* rootSaga() {
  yield all([...coin]);
}
```

![2017-11-25 (6).png](https://qiita-image-store.s3.amazonaws.com/0/99876/3da83f0a-ffb2-fc0a-e579-439ad941200c.png)

### actionの追加

同様に、`getCoinRequest`を書いて保存。

```js:src/sagas/coin.js(保存前）
import { put, call,takeLatest } from 'redux-saga/effects';
import * as actions from '../actions';
import api from '../api';

getCoinRequest

export default [];
```

```js:src/sagas/coin.js(保存後)
import { put, call, takeLatest } from "redux-saga/effects";
import * as actions from "../actions";
import api from "../api";

export function* handleGetCoinRequest(action) {
  try {
    const { data } = yield call(api.getCoinRequest, action.payload);
    yield put(actions.getCoinSuccess(data));
  } catch (error) {
    yield put(actions.getCoinFailure(error));
  }
}

export default [
  takeLatest(actions.getCoinRequest.toString(), handleGetCoinRequest)
];
```

## Axiosの作成

### 新規作成

`touch src/api/coin.js`

```js:src/api/coin.js
import axios from "../axiosConfig"
```

```js:src/api/index.js(自動生成)
import * as coin from "./coin";
const api = {
  ...coin
};
export default api;
```

### Actionの追加

```js:src/api/coin.js(保存前)
import axios from "../axiosConfig"

getCoinRequest
```

```js:src/api/coin.js(保存後)
import axios from "../axiosConfig";

export const getCoinRequest = config => axios.get(``, config);
```

### 編集

サンプルにそって以下のように書き換える。

```js:src/api/coin.js
import axios from "../axiosConfig";

export const getCoinRequest = (id, config) => axios.get(`/v1/ticker/${id}/`, config);
```

## コンテナの作成

### 新規作成

`touch src/containers/CoinsShow.js`

```js:src/contaiiners/CoinsShow.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions'

class ClassNameHere extends Component{
  render () {
    return ()
  }
}

ClassNameHere.propTypes = {}

const mapStateToProps = (state, ownProps) => {
  return {}
}
const mapDispatchToProps = dispatch => bindActionCreators(
  {},
  dispatch
)

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

### 編集

以下のようにコンテナを編集。ここではs2sは使用されない。

```js:src/containers/CoinsShow.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions'
import styled from 'styled-components'
import { Link} from 'react-router'
import {Breadcrumb,ListGroup,ListGroupItem,PageHeader,Panel} from 'react-bootstrap';

class CoinsShow extends Component{
  componentDidMount(){
   this.props.getCoinRequest(this.props.params.id)
  }
  render () {
    const { coin } = this.props
    return (
      <Wrapper>
        <PageHeader>Coins Show</PageHeader>
        <Breadcrumb>
          <Breadcrumb.Item>
            <Link to='/'>Home</Link>
          </Breadcrumb.Item>
          <Breadcrumb.Item active>
            {coin.name}
          </Breadcrumb.Item>
        </Breadcrumb>
        <Panel header={coin.name}>
          <ListGroup>
            <ListGroupItem><small>id: {coin.id}</small></ListGroupItem>
            <ListGroupItem><small>name: {coin.name} </small></ListGroupItem>
            <ListGroupItem><small>symbol: {coin.symbol} </small></ListGroupItem>
            <ListGroupItem><small>rank: {coin.rank} </small></ListGroupItem>
            <ListGroupItem><small>price_usd: {coin.price_usd} </small></ListGroupItem>
            <ListGroupItem><small>price_btc: {coin.price_btc} </small></ListGroupItem>
            <ListGroupItem><small>24h_volume_usd: {coin["24h_volume_usd"]} </small></ListGroupItem>
            <ListGroupItem><small>market_cap_usd: {coin.market_cap_usd} </small></ListGroupItem>
            <ListGroupItem><small>available_supply: {coin.available_supply} </small></ListGroupItem>
            <ListGroupItem><small>total_supply: {coin.total_supply} </small></ListGroupItem>
            <ListGroupItem><small>max_supply: {coin.max_supply} </small></ListGroupItem>
            <ListGroupItem><small>percent_change_1h: {coin.percent_change_1h} </small></ListGroupItem>
            <ListGroupItem><small>percent_change_24h: {coin.percent_change_24h} </small></ListGroupItem>
            <ListGroupItem><small>percent_change_7d: {coin.percent_change_7d} </small></ListGroupItem>
            <ListGroupItem><small>last_updated: {coin.last_updated}</small></ListGroupItem>
          </ListGroup>
        </Panel>
      </Wrapper>
    )
  }
}

CoinsShow.propTypes = {}


const Wrapper = styled.div`
  margin: 25px 40px;
`

const mapStateToProps = (state, ownProps) => {
  return {
    coin: state.coin.coin
  }
}
const mapDispatchToProps = dispatch => bindActionCreators(
  {
    getCoinRequest: bindActionCreators( actions.getCoinRequest, dispatch)
  },
  dispatch
)

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

## 動作確認

### 開発サーバ

色々エラーでているけれど確認成功。最低限動作させるために追加したソースは後述。

![2017-11-25 (9).png](https://qiita-image-store.s3.amazonaws.com/0/99876/752c9e44-856f-aa6a-f856-184dd9bc0884.png)

### ビルド

ビルドも無事成功。

```
cd docker && docker-compose run s2s yarn build
```

![2017-11-25 (10).png](https://qiita-image-store.s3.amazonaws.com/0/99876/2d13cb0c-f627-6040-6362-71789c6b0336.png)

### 追加したソース

開発サーバを動作させるためにサンプルを参考に以下のソースを追加した。

```js:src/axiosConfig.js
import axios from 'axios'

const API_ROOT = process.env.API_ROOT || 'https://api.coinmarketcap.com'

axios.defaults.timeout = 5000
axios.defaults.baseURL = API_ROOT
axios.defaults.headers.post['Content-Type'] = 'application/json'

export default axios
```

```js:src/store/configureStore.js
import { createStore, compose, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { routerMiddleware } from 'react-router-redux'
import rootReducer from '../reducers'
import rootSaga from '../sagas'

export default function configureStore(history, initialState) {

  const sagaMiddleware = createSagaMiddleware()
  const store = createStore(
    rootReducer,
    initialState,
    compose(
      applyMiddleware(
        sagaMiddleware,
        routerMiddleware(history)
      )
    )
  )
  sagaMiddleware.run(rootSaga)

  return store
}
```

```js:src/routes.js
import React from 'react'
import { Route, IndexRoute } from 'react-router';

import CoinsShow from './containers/CoinsShow'

export default (
  <Route path="/" component={CoinsShow} >
    <IndexRoute component={CoinsShow} />
    <Route path="coins/:id" component={CoinsShow} />
  </Route>
)
```

```js:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';

import { Provider } from 'react-redux'
import { Router, browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'

import routes from './routes'
import configureStore from './store/configureStore'

const store = configureStore()
const history = syncHistoryWithStore(browserHistory, store)

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

## 参考

[s2s-redux-actions-sample](https://github.com/cndlhvn/s2s-redux-actions-sample)
[さよならボイラープレート。s2sによる高速reduxアプリケーション構築](https://qiita.com/akameco/items/e1489c6bbf3439ec6ca4)
[yarnチートシート](https://qiita.com/morrr/items/558bf64cd619ebdacd3d)
[コマンド一発でReactの開発環境を構築してくれるFacebook製ツール「create-react-app」](https://mae.chab.in/archives/2910)
[Reactを秒速で使い始められるcreate-react-appの使い方と使い心地](https://aloerina01.github.io/javascript/react/2017/02/28/1.html)
[Reactチュートリアルをcreate-react-appの形式に合わせて焼きなおす](https://qiita.com/NewGyu/items/1e326b6e8d3dd2093c53)
[Yarnで react+browserify+babel+JSX変換してみる](https://qiita.com/myLifeAsaDog/items/4060ff67e71a1b5a41d1)
[create-react-appでyarnしたいけど対応してないのでreact-scriptsだけ使う](https://qiita.com/inuscript/items/d38c96c9f49fa129d8e0)