React Hooksを用いて状態管理する際、dispatchを内包したaction creatorを呼び出してみる

## この記事のポイント
hooksでグローバルな状態管理を行う記事は何番煎じですが、その多くがcomponentで直接dispatchしています。
componentからは純粋にaction creatorを呼び出したいと思い検討しました。

### 前提
Reactは16.8.6を用いています。
React、fluxについては解説していません。
ここでのaction creatorはReduxの定義と異なっているようです。ご理解ください。
https://redux.js.org/basics/actions#action-creators

## ちょっと前の話
以前ReduxとTypeScriptを用いてアプリを作ってみましたが、記述量にうんざりしてしまいました。
でも状態管理は使いこなしたいなとひとまずReduxに再入門したところ、React HooksとCotenxt APIを使えば簡単にfluxが実装できそうと思い、いろいろと参考にして実装してみました。

[以前React Hooksで状態管理したもの](https://github.com/mr-hana/github-viewer)

実装してみてcomponentがaction creatorにdispatchを渡している感じがなにか違うんじゃないかと思い、改めて考えてみました。

## 考えたこと
**案１　action creatorからdispatchを取得する**
action creatorにReactを依存させたくなかったため却下しました。
様々なところにちょっとづつ依存が入ってしまうと変更がある際に影響が出そうなので避けました。
どちらにせよCotenxt APIを使う前提なので、気にする必要はなかったかもしれません。

**案２　dispatchを内包したaction creatorを配布する**
dispatchを内包したaction creatorを作成しcontext経由で配布すれば、利用側は呼び出すだけとなり簡単になりそうです。

### 実装したもの
ということで、案２で実装しました。
サンプルのためだけに実装するのが苦手なことと、React Routerをわざわざ使うというサブテーマを設けているため、余計なものがいろいろと書いてありますがかいつまんでください。

[今回実装したもの](https://github.com/mr-hana/three-by-three-chart)

## 解説
もちろん自分でかいつまみます。
流れはmodules -> store -> componentsとなります。

**modules**
fluxの一般的なstate, reducer, actionを定義します。

**store**
modulesを元にuseReducerとcreateContextを用いて状態管理のためのStoreを生成します。
Store.Providerを介して、stateとaction creators（ついでにgetters）をReactに配布します。

**comoponents**
Reactのコンポーネントです。
useContextでaction creatorsを受け取り、各処理とマッピングします。

**その他**
pagesはReact Routerの各URLのルートとなるコンポーネントを配置しています。

### modules
内容はどうでもいいのでexportしているものだけ抜き出すとこんな感じです。
dispatchersが今回のキモです。外部からdispatchを注入してもらってaction creatorsを生成します。
ついでですが、gettersも同じ要領でstateを注入して生成します。

```javascript
// src/modules/index.js

export const initialState = {...};
export const reducers = (state, action) => {
  switch (action.type) {
    case 'CHANGE_CONTENT':
      ...
    case 'SELECT_CHART':
      ...
    case 'INIT_CHART':
      ...
    default:
      return state;
  }
}

export const dispatchers = dispatch => {
  return {
    changeContent: (position, value) => {
      dispatch({
        type: 'CHANGE_CONTENT',
        payload: { position, value }
      });
    },
    selectChart: selectedKey => {
      dispatch({ type: 'SELECT_CHART', payload: { selectedKey: selectedKey } });
    },
    selectCenterOfCharts: () => {
      dispatch({ type: 'SELECT_CHART', payload: { selectedKey: CenterKey } });
    },
    initChart: keyword => {
      dispatch({ type: 'INIT_CHART', payload: { keyword: keyword } });
    }
  };
}

export const getters = state => {
  return {
    getPositions: () => { },
    getSelectedChart: () => { },
    isCreation: () => { },
    generateRandomChart: () => { }
  };
}
```

すぐに分割やネストしたくなると思うので適宜したらいいと思います。

### store
createContextでstoreを生成します。

```javascript
export const Store = React.createContext();
```

自作のuseActions（後述）でstateとdispatchを含んだaction creatorsを生成します。

```javascript
const [state, actions] = useActions(initialState, reducers, dispatchers);
```

Providerのvalueにstate, action creators, gettersを設定して配布します。

```jsx
const value = { state, actions, getters: storeGetters };
return <Store.Provider value={value}>{props.children}</Store.Provider>;
```

全体はこんな感じです。

```jsx
// src/store/index.js

import React from 'react';
import PropTypes from 'prop-types';
import useActions from './useActions';
import { initialState, reducers, dispatchers, getters } from '../modules';

export const Store = React.createContext();
export const StoreProvider = props => {
  const [state, actions] = useActions(initialState, reducers, dispatchers);
  const storeGetters = getters(state);
  const value = { state, actions, getters: storeGetters };
  return <Store.Provider value={value}>{props.children}</Store.Provider>;
};

StoreProvider.propTypes = {
  children: PropTypes.object.isRequired
};
```

### store/useActions
useReducerを用いてstateとdispatchを生成します。
dispatchersと生成したdispatchを用いてaction creatorsを生成します。
このあたりから型が欲しくなってきて気がそぞろに。

```javascript
// src/store/useActions.js

import React from 'react';

const useActions = (initialState, reducers, dispathcers) => {
  const [state, dispatch] = React.useReducer(reducers, initialState);
  const actions = dispathcers(dispatch);
  return [state, actions];
}

export default useActions;
```

### comoponents
useContextでstateやactionsを受け取ります。

```javascript
import React, { useState, useRef, useEffect, useContext } from 'react';
import { Store } from '../store';

const ChartContent = props => {
  ...
  const { state, actions } = useContext(Store);
  ...
}
```

あとは自由にaction creatorを実行してください。

```javascript
const handleBlur = () => {
  actions.changeContent(props.position, value);
  setEditng(false);
}
```

いろいろと書いてありますがうまく避けて読んでください。
handleDoubleClickやhandleBlurの中で実行しています。
stateも利用しています。

```jsx
// src/components/ChartContent.js

import React, { useState, useRef, useEffect, useContext } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import { Store } from '../store';

const useStyles = makeStyles(theme => ({
  ...
}));

const ChartContent = props => {
  const classes = useStyles();
  const { state, actions } = useContext(Store);
  const isMiddleCenter = props.position === 'middleCenter';
  const [value, setValue] = useState(props.content.value);
  const [isEditng, setEditng] = useState(false);
  const inputElement = useRef(null);
  useEffect(() => {
    if (isEditng) {
      inputElement.current.focus();
    }
  }, [isEditng]);

  const handleSingleClick = () => {
    if (!props.isRandom && !isMiddleCenter) {
      setEditng(true);
    }
  }

  const handleDoubleClick = () => {
    setEditng(false);
    let selectedKey = props.content.key;
    if (!props.isRandom && isMiddleCenter) {
      selectedKey = state.parentKey;
    }

    actions.selectChart(selectedKey);
    props.navigateIndex();
  }

  const handleChange = inputValue => {
    setValue(inputValue);
  }

  const handleBlur = () => {
    actions.changeContent(props.position, value);
    setEditng(false);
  }

  return (
    <div className={classes.container}>
      <Paper className={classes.paper} onClick={handleSingleClick} onDoubleClick={handleDoubleClick}>
        {
          isEditng ? (
            <TextField
              inputRef={inputElement}
              fullWidth
              multiline
              value={value}
              onChange={e => handleChange(e.target.value)}
              onBlur={handleBlur} />
          ) : (
              <Typography variant="h6" component="p" className={classes.text}>
                {value}
              </Typography>
            )
        }
      </Paper>
    </div>
  );
}

export default ChartContent;
```

## 感想
当初のモヤモヤが解決されて満足です。

今回状態管理と向き合う中で湧き上がったことが２つあります。
・component（利用側）とstore（提供側）で共通のなにかを持って、actionを厳密に実行したい。
・グローバルな状態管理ってどれくらい必要なんだろう？

前者はpropTypes定義するのかな。あるいはTypeScriptにすれば解決でしょうか？
TypeScript再入門に想いを馳せてみます。

後者について、今回の場合Reactの状態管理だけを用いているのでリロードすると状態が復元しません。
そのため消えて困る場合は良いタイミングで外部に永続化する必要があります。
だったら別にpageのルート単位で必要な状態を持っていてもいいんじゃないでしょうか。
パフォーマンスとか要件とかで判断するのでしょうが、必要に応じて共有範囲を広げたらいいんだろうなと思いました。
しばらくは無闇にグローバル化しない方向で考えてみます。