この記事のポイント
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が実装できそうと思い、いろいろと参考にして実装してみました。
実装してみてcomponentがaction creatorにdispatchを渡している感じがなにか違うんじゃないかと思い、改めて考えてみました。
考えたこと
案1 action creatorからdispatchを取得する
action creatorにReactを依存させたくなかったため却下しました。
様々なところにちょっとづつ依存が入ってしまうと変更がある際に影響が出そうなので避けました。
どちらにせよCotenxt APIを使う前提なので、気にする必要はなかったかもしれません。
案2 dispatchを内包したaction creatorを配布する
dispatchを内包したaction creatorを作成しcontext経由で配布すれば、利用側は呼び出すだけとなり簡単になりそうです。
実装したもの
ということで、案2で実装しました。
サンプルのためだけに実装するのが苦手なことと、React Routerをわざわざ使うというサブテーマを設けているため、余計なものがいろいろと書いてありますがかいつまんでください。
解説
もちろん自分でかいつまみます。
流れは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を注入して生成します。
// 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を生成します。
export const Store = React.createContext();
自作のuseActions(後述)でstateとdispatchを含んだaction creatorsを生成します。
const [state, actions] = useActions(initialState, reducers, dispatchers);
Providerのvalueにstate, action creators, gettersを設定して配布します。
const value = { state, actions, getters: storeGetters };
return <Store.Provider value={value}>{props.children}</Store.Provider>;
全体はこんな感じです。
// 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を生成します。
このあたりから型が欲しくなってきて気がそぞろに。
// 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を受け取ります。
import React, { useState, useRef, useEffect, useContext } from 'react';
import { Store } from '../store';
const ChartContent = props => {
...
const { state, actions } = useContext(Store);
...
}
あとは自由にaction creatorを実行してください。
const handleBlur = () => {
actions.changeContent(props.position, value);
setEditng(false);
}
いろいろと書いてありますがうまく避けて読んでください。
handleDoubleClickやhandleBlurの中で実行しています。
stateも利用しています。
// 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;
感想
当初のモヤモヤが解決されて満足です。
今回状態管理と向き合う中で湧き上がったことが2つあります。
・component(利用側)とstore(提供側)で共通のなにかを持って、actionを厳密に実行したい。
・グローバルな状態管理ってどれくらい必要なんだろう?
前者はpropTypes定義するのかな。あるいはTypeScriptにすれば解決でしょうか?
TypeScript再入門に想いを馳せてみます。
後者について、今回の場合Reactの状態管理だけを用いているのでリロードすると状態が復元しません。
そのため消えて困る場合は良いタイミングで外部に永続化する必要があります。
だったら別にpageのルート単位で必要な状態を持っていてもいいんじゃないでしょうか。
パフォーマンスとか要件とかで判断するのでしょうが、必要に応じて共有範囲を広げたらいいんだろうなと思いました。
しばらくは無闇にグローバル化しない方向で考えてみます。