ポートフォリオちゃんの命の輝きを見よ！

# 前置き
[前回の記事](https://qiita.com/roottool/items/89b4adcea8314e5f8e17)から、ポートフォリオが色々変わりました。
色々変えた際のインプットをアウトプットしていこうと思います。

## 具体的には何が変わった？
- redux, react-reduxの導入
- connected-react-routerを導入
- redux-sagaの導入
- reactstrapからMaterial-UIに変更
- CSS→SASS→styled-componentsに流れ着く

## この記事で書くこと
この記事では、下記の3点に絞って書きます。

- redux, react-reduxの導入
- connected-react-routerを導入
- redux-sagaの導入

ポートフォリオは[React + Redux + TypeScriptの最小構成](https://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c)を写生して作り変えました。
そのため、[React + Redux + TypeScriptの最小構成](https://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c)に対してどのようにconnected-react-routerやredux-sagaを実装したかに焦点を当てたいと思います。

# 完成品
[roottool's Portfolio](https://roottool.github.io/)
[ソースコード](https://github.com/roottool/roottool.github.io)

画面サンプルは[前回の記事](https://qiita.com/roottool/items/89b4adcea8314e5f8e17#pcサイズ)をご覧下さい。

# 開発環境
- Windows 10 Pro
- Visual Studio Code
- Node.js：v10.13.0
- npm: v4.0.5

# 使ったパッケージ
多いので`package.json`の`dependencies`と`devDependencies`を載せます。

```package.json
"dependencies": {
    "@material-ui/core": "^3.5.1",
    "@material-ui/icons": "^3.0.1",
    "@types/jest": "23.3.9",
    "@types/node": "10.12.2",
    "@types/react": "^16.7.11",
    "@types/react-dom": "16.0.9",
    "@types/react-helmet": "^5.0.7",
    "@types/react-redux": "^6.0.10",
    "@types/react-router-dom": "^4.3.1",
    "@types/styled-components": "^4.1.2",
    "axios": "^0.18.0",
    "connected-react-router": "^5.0.1",
    "node-sass": "^4.9.4",
    "react": "^16.6.0",
    "react-dom": "^16.6.0",
    "react-helmet": "^5.2.0",
    "react-icons": "^3.2.2",
    "react-redux": "^5.1.1",
    "react-router-dom": "^4.3.1",
    "react-scripts": "2.1.1",
    "redux": "^4.0.1",
    "redux-saga": "^0.16.2",
    "styled-components": "^4.1.2",
    "typescript": "3.1.6"
  },
  "devDependencies": {
    "gh-pages": "^2.0.1",
    "redux-devtools-extension": "^2.13.6"
  }
```

# redux, react-reduxの導入
reduxは、状態(State)管理を行うパッケージです。
そのreduxをReactで使用出来るようにするパッケージがreact-reduxです。
reduxの大まかな流れは以下のようになります。
![ReduxFlow.gif](https://qiita-image-store.s3.amazonaws.com/0/300336/0e34773f-5883-03f1-ca5a-159b0c05ee88.gif)
引用元：[Redux. From twitter hype to production](http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/27)

Reactでは各コンポーネント単位で状態を管理することが出来ます。

```example.tsx
interface IState {
    testState: boolean;
}

export default class Example extends Component<{}, IState> {
    constructor(props: IProps) {
        super(props);

        this.state = {
            testState: false
        };
    }

    private changeState = () => this.setState({testState: true});
    ...
}
```
しかし、コンポーネントの数が多くなるに従って各コンポーネントの状態を把握するのは難しくなります。(コンポーネント数が数個ではなく10個、数十個となった時に、全てのコンポーネントの状態管理をメンテナンスするのは辛いと思います。)
そこで、**各コンポーネントの状態を一元管理しよう**というのがReactにおけるreduxの使い方になります。

## 参考資料
reduxについては、以下の記事を参考にしました。
上から順に読んでいくことをオススメします。
~~自分でreduxとは何かを書かなくても、以下の記事を読んでもらえば良いかなと思いました~~

- [React Redux の SPA を運用して得られた知見と実装例、開発フローもあるよ！ - redux](https://qiita.com/numanomanu/items/af97312f34cf1388cee6#redux)
- [たぶんこれが一番分かりやすいと思います React + Redux のフロー図解](https://qiita.com/mpyw/items/a816c6380219b1d5a3bf)
- [React + Redux + TypeScriptの最小構成](https://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c)

[React + Redux + TypeScriptの最小構成](https://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c)を写生したので、今回のポートフォリオでのreduxの実装方法については説明は割愛します。

# connected-react-routerの導入
connected-react-routerは、react-routerでのページ遷移の状態管理をreduxで行うパッケージです。
こういったルーティングの状態管理には、react-router-reduxを使用してる方が多いと思います。
しかし、[react-router-reduxの使用を非推奨とするので、connected-react-routerを使ってください](https://github.com/reactjs/react-router-redux#project-deprecated)と公式ドキュメントに記載があったので使用しました。

## 導入方法
以下のコマンドを実行してインストールします。

```
npm install --save connected-react-router
```

## 実装方法
### 実装、その前に
[公式ドキュメント](https://github.com/supasate/connected-react-router#usage)には実装するにはStep 1とStep 2を行う必要があると書かれています。
Step 1には以下のように書かれています。
1. `history`オブジェクトを作る
2. `root reducer`ファンクションを作って、`history`オブジェクトを引数にする。
3. 引数の`histroy`オブジェクトを使って`root reducer`内に`router reducer`を追加して`connectRouter`に渡す。**注意: `router`キーを必ず付ける。**
4. (`push('/path/to/somewhere')`)のような`history`アクションをディスパッチしたい場合は、`routerMiddleware(history)`を使用する。


```reducers.js
import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'

export default (history) => combineReducers({
  router: connectRouter(history),
  ... // rest of your reducers
})
```
```configureStore.js
...
import { createBrowserHistory } from 'history'
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'connected-react-router'
import createRootReducer from './reducers'
...
const history = createBrowserHistory()

const store = createStore(
  createRootReducer(history), // root reducer with router state
  initialState,
  compose(
    applyMiddleware(
      routerMiddleware(history), // for dispatching history actions
      // ... other middlewares ...
    ),
  ),
)
```

上記に対し、`connected-react-router`を導入する前のポートフォリオではstoreのコードが以下のようになっていました。

```store.ts
import { createStore, combineReducers, Action } from 'redux'
import app, { AppActions, AppState } from "./module";

export default createStore(
  combineReducers({
    app
  })
)

export type ReduxState = {
    app: AppState;
};

export type ReduxAction = Action | AppActions;
```
`root reducer`がありませんが、実態は`combineReducers()`で各reducerの結合のみを行っているとソースから読み取ったので、Step 1の2番目に書かれている`root reducer`ファンクションを作成せずに実装します。

### Step 1の実装
まず、結果を記載します。

```store.ts
import {
    Action,
    applyMiddleware,
    compose,
    createStore,
    combineReducers
} from "redux";
import { createBrowserHistory } from "history";
import {
    RouterState,
    routerMiddleware,
    connectRouter,
    RouterAction
} from "connected-react-router";
// redux関連
import app, { AppActions, AppState } from "./module";
// ポイント1
export const history = createBrowserHistory();

export default createStore(
    combineReducers({
        // ポイント2
        router: connectRouter(history),
        app
    }),
    // ポイント3
    compose(applyMiddleware(routerMiddleware(history)))
);

export type ReduxState = {
    // ポイント4
    router: RouterState;
    app: AppState;
};

// ポイント3
export type ReduxAction = Action | RouterAction | AppActions;
```
まずポイント1ですが、`history`オブジェクトを作成します。`export`している理由は後述します。
ポイント2は、`router`キーを忘れず記入して`history`オブジェクトを`connectRouter()`に渡します。
ポイント3は、`routerMiddleware(history)`をミドルウェアを使ってreduxのStoreに接続します。
最後のポイント4は、全体の`State`と`Action`の型定義に`connected-react-router`の`RouterState`と`RouterAction`を追加してStep 1の部分は完了です。

### Step 2の実装
Step 2部分の実装は以下のようになります。

```index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { ConnectedRouter } from "connected-react-router";

import App from "./Container";

import store, { history } from "./store";

ReactDOM.render(
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <App />
        </ConnectedRouter>
    </Provider>,
    document.getElementById("root") as HTMLElement
);
```
`Provider`と`ConnectedRouter`に対して`store.ts`で作成した`store`と`export`した`history`を渡して実装完了です。

# redux-sagaの導入
redux-sagaは[redux, react-reduxの導入](https://qiita.com/drafts/e0eb5a9385566e9efd78/edit#redux-react-redux%E3%81%AE%E5%B0%8E%E5%85%A5)で引用した図のMiddlewareの部分になるパッケージです。
今回は[Steam Web API](https://developer.valvesoftware.com/wiki/Steam_Web_API)からSteamで所有しているゲーム一覧を取得して表示するという、非同期処理を実行するために使用しました。
Steam Web APIの使用方法は本題から逸れるため、割愛します。

## 参考資料
redux-sagaについては、以下の記事を参考にしました。

- [redux-sagaで非同期処理と戦う](https://qiita.com/kuy/items/716affc808ebb3e1e8ac)
- [公式ドキュメント](https://github.com/redux-saga/redux-saga/blob/master/README_ja.md)
- [公式サンプル(async)](https://github.com/redux-saga/redux-saga/tree/master/examples/async)

## 導入方法
以下のコマンドを実行してインストールします。

```
npm install --save redux-saga
```

## 実装方法
### 想定している動作
Hobbiesページを開いた時にSteam Web APIからSteamで所有しているゲーム一覧を取得して表示することです。
これは以下のようになります。

```/src/Pages/Hobbies/index.tsx
class Hobbies extends Component<IProps, {}> {
    constructor(props: IProps) {
        super(props);

        if (this.props.value.rows && this.props.value.rows.length === 0) {
            this.props.actions.requestFetchingUserOwnedGameInfo();
        }
    }
    ...
}
```
Hobbiesページを初めて開いた時だけ実行したいため、`if`文で囲っています。

### Sagaを作成する
まずは、実装結果を以下に示します。

```/src/sagas/hobbies.ts
import { take, put, call, fork } from "redux-saga/effects";
import axios from "axios";
import {
    ActionNames,
    IUserOwnedGames,
    IGamesInfo,
    receiveFetchedUserOwnedGameInfo
} from "../Pages/Hobbies/module";

// プレイ時間降順ソート
const sortOwnedGames = (ownedGames: IUserOwnedGames) => {
    return ownedGames.response.games.sort(
        (leftSide: IGamesInfo, rightSide: IGamesInfo): number => {
            if (leftSide.playtime_forever > rightSide.playtime_forever) {
                return -1;
            } else if (leftSide.playtime_forever < rightSide.playtime_forever) {
                return 1;
            }
            return 0;
        }
    );
};

const fetchOwnedGamesApi = () => {
    const id = "roottool";
    const url = `https://example.com/api/?id=${id}/`;
    return axios
        .get(url)
        .then(response => {
            return response.data;
        })
        .catch(error => {
            throw new Error(error);
        });
};

export function* fetchUserOwnedGameInfo() {
    yield take(ActionNames.REQUEST_FETCH);
    const ownedGames = yield call(fetchOwnedGamesApi);
    const sortedOwendGames = yield call(sortOwnedGames, ownedGames);
    yield put(receiveFetchedUserOwnedGameInfo(sortedOwendGames));
}

export default function* root() {
    yield fork(fetchUserOwnedGameInfo);
}
```
順を追って説明します。
まず。`fetchUserOwnedGameInfo`というGenerater関数を実行するタスクを`fork`によって作成します。
Generator関数は、`function*`で定義されます。
Generator関数で定義する理由は、`yield`を使って処理の完了を待つことが出来るためです。
作成されたタスクで実行される`fetchUserOwnedGameInfo`は、以下の順で処理を行います。

1. `take()`で`/src/Pages/Hobbies/index.tsx`の`this.props.actions.requestFetchingUserOwnedGameInfo();`が実行されるのを待つ。
2. `fetchOwnedGamesApi`でAPIから所有ゲーム一覧を取得する。
3. 2番の処理完了待ち
4. 2番の処理によって取得した所有ゲーム一覧を、`sortOwnedGames`がプレイ時間で降順ソートする。
5. 4番の処理完了待ち
6. 4番の処理によってソートされた所有ゲーム一覧を、`receiveFetchedUserOwnedGameInfo`で`Store`に格納する。

ここで躓いた点が1つあります。
`fetchOwnedGamesApi`で取得した所有ゲーム一覧が`ownedGames`に格納されるはずなのに、`undefined`になって`sortOwnedGames`でエラーが発生したことです。
原因は、`return response.data;`の`return`を書いていなかったからでした。
`return axios.get(url).then(...){...}.catch(...){...}`とreturn文をかいていたので、`then(...){...}`か`.catch(...){...}`の結果が`return`によって渡されると勘違いしていました。
だからといって`return axios.get(url).then(...){...}.catch(...){...}`の`return`を外すとエラーが発生しました。
このことから私は`Promise`が含まれるメソッドを`yield call()`した場合、`Promise`部分を`return`することで`Promise`部分を`yield`によって待つように変化するのだと解釈しました。

### 作成したSagaとReduxのStoreと接続する
最後に作成したSagaを、`redux-saga`ミドルウェアを使ってreduxのStoreに接続します。
まずは、[公式ドキュメント](https://github.com/redux-saga/redux-saga/blob/master/README_ja.md#mainjs)の実装例を以下に示します。

```main.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// ポイント1：Saga ミドルウェアを作成する
const sagaMiddleware = createSagaMiddleware()

// ポイント2：Store にマウントする
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

// ポイント3：Saga を起動する
sagaMiddleware.run(mySaga)

// アプリケーションのレンダリング
```

Javasriptですが、Typescriptであっても実装する内容は同じです。
しかし実装例と違って今回のポートフォリオは、`store.ts`にStore部分を分離しています。
従って、今回の実装に合わせた結果は以下のようになります。(関連部分以外省略しています)


```/src/store.ts
...
import createSagaMiddleware from "redux-saga";
...
// ポイント1：Saga ミドルウェアを作成する
export const sagaMiddleware = createSagaMiddleware();

export default createStore(
    combineReducers({
        router: connectRouter(history),
        app,
        hobbies
    }),
    // ポイント2：Store にマウントする
    compose(applyMiddleware(routerMiddleware(history), sagaMiddleware))
);
...
```
ポイント1としていたSagaミドルウェアを作成する部分を`export`していますが、理由は後述します。
作成したSagaミドルウェアをポイント2のように、`applyMiddleware()`の`()`内に追加したら`store.ts`に対する実装作業は終了です。
次はポイント3で書かれているように、Sagaを起動させます。
今回のポートフォリオでは、`/src/`直下にある`index.tsx`で起動させています。
実装部分を以下に示します。(redux-saga実装に関連していないimport文は省略しています)

```/src/index.tsx
...
import rootSaga from "./sagas";

// ポイント3：Saga を起動する
sagaMiddleware.run(rootSaga);

// アプリケーションのレンダリング
ReactDOM.render(
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <App />
        </ConnectedRouter>
    </Provider>,
    document.getElementById("root") as HTMLElement
);
```
`store.ts`で`export`していた`sagaMiddleware`の`run()`メソッドにて、作成したSagaを起動させて完了です。

# 実装した感想
reduxを学習することに対して、大変そうだと抵抗感がありました。
しかし、使ってみるとStoreの一元管理によってコンポーネントの複雑さが軽減されたので学習コストをかけた分のメリットがあったと感じました。

# 最後に
Steamerの皆さん、ポートフォリオを作ってご自慢のSteamライブラリを全世界に晒しましょう！