筆者は、 Reactも Reduxも React-Reduxも初心者で、プライベートでちょっと触っている程度のペーペーです。
React に Hooks が導入されてしばらく経ちますが、React-Reduxにも Hooks が導入されていることを最近知りました。ちょっと試して見たところ、Hooksを使うことで少しだけモヤモヤが晴れてきたので、これまで理解したことを整理したいと思います。
useState, useReducer は(規模の大きな開発には)使わない
useState
, useReducer
はとっつきやすくてわかりやすいです。なので、ちょっとしたプログラムでは使うと思います。便利です。YoutubeのReact Today and Tomorrow and 90% Cleaner React With Hooks で、初めてこれを知った時は、感動すら、覚えました。
ですが、今回、自分でちょっと触ってみて、複数のコンポーネントにまたがるような情報を管理する必要が出てくると面倒そうだと感じました。
また、 テストも面倒なんじゃないかなと思ったりしてます。
以下が、毎度お馴染みの、 useState
を使った実装例です。
import React, { useState } from "react";
const ComponentUseState = () => {
const [num, setNum] = useState(0);
return (
<div>
<h2>Using useState</h2>
Number: {num}
<button onClick={() => setNum(num + 1)}>+</button>
<button onClick={() => setNum(num - 1)}>-</button>
</div>
);
};
export default ComponentUseState;
シンプルに書けるのですが、足し算、引き算の処理(ロジック)が、 コンポーネントの中に含まれている状態で、ちょっとテストが面倒そうです。
また、別のコンポーネントから、 num
を参照したり、変更したりするのが面倒そうです。
useReducer
を使うとこんな感じになります。
import React, { useReducer } from 'react'
const initialState = {num: 0};
const reducer = (state, action) => {
switch(action.type) {
case 'decrement':
return {...state, num: state.num - 1}
case 'increment':
return {...state, num: state.num + 1}
default:
return state;
}
}
const ComponentUseReducer = () => {
const [state, dispatch] = useReducer(reducer, initialState)
const { num } = state
return (
<div>
<h2>Using useReducer</h2>
Number: {num}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</div>
);
};
export default ComponentUseReducer;
useState
を使うより、コードが長くなってしまいますが、 useReducer
を使うことによって、 足し算、引き算のロジックの部分は、reducer
関数の中に閉じ込めて、コンポーネントの ComponentUseReducer
とは、分離することができます。
reducer
は、純粋な JavaScript のコードなので、単体で実行することができ、テストもしやすそうです。(本来、reducer は、コンポーネントとは別ファイルに分割すべきです。ここでは、敢えて分割しておりません。)
const reducer = (state, action) => {
switch(action.type) {
case 'decrement':
return {...state, num: state.num - 1};
case 'increment':
return {...state, num: state.num + 1};
default:
return state;
}
}
例えば、以下のようにChrome のデベロッパーツールの console でも単独で実行できます。
コードの中で、初期状態を示しているのが initialState
です。この状態をボタンが押されるなどのイベントによってどう変化させていくのかというロジックの部分が reducer になります。ですから、この reducer の部分と initialState をどうするかは実際の開発では自分で考えて実装しないといけないところですね。(私はこのことに気づく(理解する)のに、随分と、時間がかかりました。)
ただ、 reducer
関数の書き方には、決まり事があります。
- 引数は、state と action の2つ。
-
action
は、type
で、state をどう変更させるかを指定する。 - 戻り値は、変更後の新しい state にする。
ということで、コンポーネントとロジックを分けることができたのですが、やっぱり、 num
を複数コンポーネントで共有するのはちょっと面倒そうです。
React-Redux の useSelector と useDispatch を使う。 connect は使わない。
そこで、 Redux と Hooks 時代の React-Redux です。
Redux と React-Redux を使うとこんな感じになります。
import React from "react";
import { createStore } from "redux";
import { Provider, useSelector, useDispatch } from "react-redux";
const initialState = { num: 0 };
const reducer = (state, action) => {
switch (action.type) {
case "decrement":
return { ...state, num: state.num - 1 };
case "increment":
return { ...state, num: state.num + 1 };
default:
return state;
}
};
const store = createStore(reducer, initialState);
const ComponentUseReactRedux = () => {
return (
<div>
<h2>ComponentUseReactRedux</h2>
<Provider store={store}>
<ChildComponentUseReactRedux />
</Provider>
</div>
)
}
const ChildComponentUseReactRedux = () => {
const num = useSelector(state => state.num);
const dispatch = useDispatch();
return (
<div>
<h3>Using useSelector, useDispatch</h3>
Number: {num}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
};
export default ComponentUseReactRedux;
随分と長くなってしまいました
initialState
と reducer
は、 useReducer
を使った例 src/ComponentUseReducer.js
と同じです。初期値とロジックなので、ここは変わりません。
const store = createStore(reducer, initialState);
で、情報の格納先を1つ作ります。イメージ的には、これを複数のコンポーネントから参照する感じです。これはお約束の1つです。
ComponentUseReactRedux
では、 React-Redux で提供される Provider
を使って、 以下のように ComponentUseReactRedux
の子コンポーネントからも、 store
の情報にアクセスできるようにします。これもお約束ごとです。
<Provider store={store}>
<ChildComponentUseReactRedux />
</Provider>
子コンポーネントの ChildComponentUseReactRedux
の中では、 useSelector
を使って num
の値を参照できるようにします。
const num = useSelector(state => state.num);
ここで、登場する、 state は、 reducer 関数の引数の state と同じものです。以下のように書き換えた方がわかりやすいかも知れません。
const selector = state => {
return state.num;
}
const num = useSelector(selector);
また、 useDispatch()
を使って、 dispatch
を取得します。
const dispatch = useDispatch()
ChildComponentUseReactRedux
の残りの部分は、 useReducer
を使った場合と同じですね。
で、この書き方だと、複数のコンポーネントから、同じ num
を参照できるのです。
試しに、 ComponentUseReactRedux
の中の ChildComponentUseReactRedux
を2つにしてみます。
const ComponentUseReactRedux = () => {
return (
<div>
<h2>ComponentUseReactRedux</h2>
<Provider store={store}>
<ChildComponentUseReactRedux />
<ChildComponentUseReactRedux />
</Provider>
</div>
)
}
するとその2つの子コンポーネントの num
の値は連動して変化します。
まとめ
-
useState
やuseReducer
は 規模の大きな開発には使わない。 - 初期ステータスをどう定義するか、また、
reducer
によってその初期ステータスをどう変化させるか(ロジック)は自分で考えて実装しないといけない。 -
createStore
を使ってステータスの格納先を作成する。 -
Provider
を使ってコンポーネント間で、ステータスを共有。 -
useSelector
を使って、ステータスのどの値を参照するのか指定する。 -
useDispatch
を使って、dispatch
を取得する。 -
connect
は使わない。 (mapStateToProps とか mapDispatchToProps とか私には難しい...。)
まだ整理できていないこと
-
useContext
を使う方法もありそうなのですが、良く調べてません。 - redux-thunk や redux-saga などの middleware の部分をどうするのが良いのか、まだ自分の中で整理ができていません。 (
useCallback
が使えそうですが...)
追記
今回は、 dispatch の引数(Action)を素のまま書きましたが、この部分を関数化するのが、より正しいお作法なのかも知れません。
const increment = () => {
return {
type: 'increment'
};
}
const decrement = () => {
return {
type: 'decrement'
};
}
を定義して、 dispatchを使うときに、 dispatch(increment())
や dispatch(decrement())
のようにします。