React.jsのhooks APIを使ってReduxのような事をしてみた

どうも、初めての方は初めまして、そうじゃない方はお久しぶりです、ヒロです！
この記事は SmartDrive Advent Calendar 2018 - Qiita の9日目です。
今回は `16.7.0-alpha.0` で追加されたhooks APIを使用して `react-redux` に似たような実装のサンプルを作成したのでご紹介します！

後一瞬だけ告知させてください、来たる　`2018年12月30日(日)、コミックマーケット95 2日目(日) 東ト38b` にて`これから始める ♡React.js 実践マニュアル` という新刊出します。
過去本もいくつか持参するのでもし良ければ下記のページを見てみてください。
[過去本一覧ページ](http://takahiro-saeki.github.io/new-book/template/index.html)
そして今回する紹介する内容はその新刊で紹介する予定だったけど、紙面オーバーして断念してしまった内容になります。汗

## Reach The Sky, Without Redux
[今回はHacker NewsのAPI](https://hn.algolia.com/api) を使用して、簡易的なHacker Newsクライアントを作成しました。
下記がサンプルの画面になります。

<img width="1440" alt="スクリーンショット 2018-12-09 21.06.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/79274/91dbe4e9-d073-6c56-145f-4f41eb4d8ff1.png">

こちらの[リポジトリ](https://github.com/takahiro-saeki/react-example-code) から手元にクローンしてから `yarn start:chapter2` をターミナルで叩く事で上記の画面を確認出来ます。

### 基本的な構成
リポジトリを見てもらうとわかりますが、基本的な構成は下記のようになっています。

```
./chapter2
├── index.html
└── src
    ├── Context.jsx
    ├── Provider.jsx
    ├── actions
    │   └── index.js
    ├── components
    │   ├── Header
    │   │   ├── index.jsx
    │   │   └── style.js
    │   ├── List
    │   │   ├── index.jsx
    │   │   └── style.js
    │   ├── ListArea
    │   │   ├── index.jsx
    │   │   └── style.js
    │   └── index.js
    ├── connect.js
    ├── containers
    │   └── App
    │       └── index.js
    ├── index.js
    ├── modules
    │   ├── fetchLogic.js
    │   ├── getFavoItems.js
    │   ├── getSearch.js
    │   └── useWindowWidth.js
    ├── pages
    │   └── MainView
    │       └── index.jsx
    ├── reducers
    │   ├── combineReducers.js
    │   ├── favorite.js
    │   ├── index.js
    │   └── posts.js
    └── store.js
```
一つ一つ解説して行きます。

### Context.jsxについて
`Context.jsx` はContextを作成しているファイルになります。hooksを使用すると従来のConsumerの記述よりも少し記述方法が楽になります。

```jsx
import { useContext } from 'react';
import Context from './Context';

const store = () => {
  const { store, dispatch } = useContext(Context);
  return {
    getState: store,
    dispatch
  };
};

export default store;
```

またさらっとstore内のコードを紹介してしまいましたが、 `store.js` ファイル内ではContextを読み込んで `getState` と `dispatch` を返しています。

### Provider.jsxについて
`react-redux` で提供されているProviderのような役割を担っています。

```jsx

import React, { useState, useReducer, useEffect } from 'react';
import Context from './Context';

type Props = {
  children: any,
  reducer: () => {}
};

const Provider = ({ children, reducer }: Props) => {
  const [store, dispatch] = useReducer(reducer);
  const [state, setState] = useState({ isLoaded: false });

  useEffect(() => {
    dispatch({ type: '@init' });
    setState({ isLoaded: true });
  }, []);

  return (
    <Context.Provider value={{ dispatch, store }}>
      {state.isLoaded ? children : false}
    </Context.Provider>
  );
};

export default Provider;
```

`Provider` コンポーネントでは `props` で`children` と `reducer` を受け取っています。 `provider` コンポーネントがレンダリングされたら `useEffect` 経由で `dispatch({ type: '@init' });` を発火して、 `reducer` 内で返しているオブジェクト(initialStore)をstoreに反映します。反映が完了したら `setState` で `isLoaded` を `true` にし、 `children` をレンダリングします。

### reducersについて
`reducers` には主に2つの役割があります。
- storeに反映させる為のreducer
- reducerを束ね上げるcombineReducers

storeに反映させる為のreducerとは通常のreducerになります。今回のサンプルの中で例を上げると `posts.js` などですね。

```js
const init = {
  data: []
};

const posts = (state = init, action) => {
  if (action.type === 'ADD_POST') {
    return { ...state, data: action.payload };
  }
  return state;
};

export default posts;
```
また `combineReducers` については `reducer` を一つの `reducer` にまとめる役割を持ちます。

```js

const combineReducers = reducer => {
  return (state = {}, action) => {
    const keys = Object.keys(reducer);
    const nextReducers = {};
    for (let i = 0; i < keys.length; i++) {
      const invoke = reducer[keys[i]](state[keys[i]], action);
      nextReducers[keys[i]] = invoke;
    }
    return nextReducers;
  };
};

export default combineReducers;
```
actionが発火されるとreducer内の全てのケースが発火されて該当するreducerには該当したreducer内の処理が行われ、それ以外は state内に保持してある値がそれぞれ割り振られているので、割り振った値がそのまま返されます。

### connect.jsについて
`connect.js` についてはconnectを `React Component` に適用した際に `mapStateToProps` と `mapDispatchToProps` を適用させる為の処理が記述されています。

```js
import React, { useContext } from 'react';
import Context from './Context';

const connect = (mapState, mapDispatch) => {
  return WrappedComponent => {
    return () => {
      const { store, dispatch } = useContext(Context);
      return (
        <WrappedComponent {...mapState(store)} {...mapDispatch(dispatch)} />
      );
    };
  };
};

export default connect;
```
特に難しい事はしてなく、至ってシンプルなHOCになっています。

### actionsについて
`actions` は `action` の記述がされています。

```js

export const addTodo = (payload: Array<Object>) => ({
  type: 'ADD_POST',
  payload
});

export const addFavoItem = (payload: number) => ({
  type: 'ADD_FAVORITE_ITEM',
  payload: {
    id: payload
  }
});
``` 

### componentsにとpagesについて
これはUI用のコンポーネントが記述されています。通常の `React Component` が記述されている為、ここでは割愛します。ちなみに `pages` は各page毎の statelessなコンポーネントを置く場所、という立ち位置ですが、今回の場合は1ページしかないので、あんまり分ける意味はなかったです。が、念の為補足します。

### modules
ここには非同期周りの処理が記述されています。本当はredux-sagaみたいな事をする、もしくはredux-sagaと互換性を持たせたかったんですが、時間の都合上断念し、非同期周りの記述を `modules` 配下に設置しています。

### containersについて
`containers` 内の `App` 内で下記のような記述をしています。

```jsx

import React, { useEffect, useCallback } from 'react';
import MainView from '@chapter2/pages/MainView';
import useWindowWidth from '@chapter2/modules/useWindowWidth';
import { getSearch, getFavoItems } from '@chapter2/modules/fetchLogic';
import { addTodo, addFavoItem } from '@chapter2/actions';
import connect from '../../connect';

const App = ({ posts, addTodo, addFavoItem, favorite }) => {
  const windowWidth = useWindowWidth();

  const invokeSearch = async ({ searchValue, keyCode, type = 'default' }) => {
    if (keyCode === 13) {
      const search = await getSearch(searchValue);
      addTodo(search.hits);
      return;
    }

    if (type === 'submit') {
      const search = await getSearch(searchValue);
      addTodo(search.hits);
      return;
    }
  };

  const invokeFavoItems = async () => {
    const getItems = await getFavoItems(favorite);
    const addingFavoFlag = await getItems.map(item => ({
      ...item,
      isFavorite: true
    }));
    await addTodo(addingFavoFlag);
  };

  const memoizePosts = useCallback(
    () => {
      const mapItem = posts.map(item => {
        const checkFavo = favorite.includes(
          item.id ? item.id : Number(item.objectID)
        );
        return {
          ...item,
          isFavorite: checkFavo
        };
      });
      return mapItem;
    },
    [posts, favorite]
  );

  useEffect(() => {
    (async () => {
      try {
        const data = await getSearch();
        addTodo(data.hits);
      } catch (e) {
        // error logic
      }
    })();
  }, []);

  const hundlers = {
    invokeSearch,
    posts: memoizePosts(),
    windowWidth,
    addFavoItem,
    invokeFavoItems
  };

  return <MainView {...hundlers} />;
};

const mapStateToProps = store => ({
  posts: store.posts.data,
  favorite: store.favorite.favorite_posts
});

const mapDispathToProps = dispatch => ({
  addTodo: param => dispatch(addTodo(param)),
  addFavoItem: param => dispatch(addFavoItem(param))
});

export default connect(
  mapStateToProps,
  mapDispathToProps
)(App);

```
整理されていない部分も多いですが、まず `App` コンポーネントの `props` に `mapStateToProps` と `mapDispathToProps` でpropsに埋め込む指定をした `props` を指定します。 `invokeSearch` と `invokeFavoItems` 検索した際とお気に入りした記事を一覧で表示させる際に非同期でリクエストしてデータを取得する為の記述です。 `memoizePosts` については `useCallback` を使用して、memoizationをしています。 また `useEffect` 内で初期のデータをリクエストして取得し、 `addTodo` action経由でstoreに反映させます。

最終的に子コンポーネントpropsを渡す為に `hundlers` というオブジェクトに渡すpropsをまとめ、 `MainView` コンポーネントにpropsを渡しています。

最後に `connect` する事によって完了です。

## 最後に
如何でしたでしょうか？かなりざっくりした形で紹介してしまいましたが、hooks APIを使用する事で簡易的なreduxのような実装が可能になりました。まだalphaリリースの段階ですが、これが正式にリリースされたら `React Class Component` を記述する回数が今までよりも減りそうな気がします。

smartdriveでは[Reactが大好きなフロントエンドエンジニアを募集しています！](https://smartdrive.co.jp/careers/)