概要
Hooks対応前のReact(v16.6.3)および、react-redux(v5.1.1)、redux(3.6.0)のプロダクションコードを触る機会があり、色々学んだ内容などをメモる。
2017~2018年あたりに利用されていた技術を改めて調べているため、考古学と表現しています。
ここまでのあらすじ
Redux自体はFulxを派生させたアーキテクチャであり、JavaScriptのデータをどう管理するかという設計手法であることがわかりました(唐突)
では、React単品ではUIを描画するだけになり状態に応じた再描画ができないため、ここにreduxを共存させるため「react-redux」がどう活用されるのかについて学習した内容をまとめていきます。
Reduxとは
Redux自体はFulxを派生させたアーキテクチャであり・・・(割愛)
下記がわかりやすかったです。
参考 : https://zenn.dev/harukaeru/articles/45d2579493a5c4
実際にコード書いてる際、Store内部の値を確認したい場合は、store.getState()で最新のstateを取得できる。
※ デバッグ用のコードを仕込んでReduxDevTool以外で確認するという手もある
// NOTE: store/index.js
const initState = { count : 50, posts: 5 };
const reducer = (state = initState ) => { return state };
const store = createStore(reducer);
console.log(store.getState()); // { count : 50, posts: 5 }
ただし、後で記載しているconnect関数や、
useSelectorを利用している時のように、UIに即座変更されるわけではない。
んで、react-reduxはなにしとるん?
結論から言うとHooksの場合は「useSelector」と「useDispatch」、非Hooksの場合は「Connect」と「mapDispatchToProps」と「mapStateToProps」を提供することで「React」と「Redux」間の状態取得・変更を可能としてくれる。
StoreのデータをUIが取得するまで
通常のReactだと、コンポーネントがStateを持ち、親からのPropsを受け取る形だが、
react-reduxでは、State管理をReduxで行っているため、Reactの各コンポーネントは親からのPropsを受け取ることしかできない。
そのため、Connectを利用する形となる。
connect関数はreact-reduxが提供する関数で、connect関数がStoreのStateを取得できるようにしている。
connectには、mapStateToProps(描画のための値をstoreから持ってくる役割)と、mapDispatchToPropsは(ユーザの動作があった時の関数を渡す役割)が存在しており、
mapStateToPropsはstore内で設定したstateを、コンポーネントにデータを渡すためのPropsへの変換します。
// NOTE : state.todosの内容を、todoという名称で取得できるようにする。
// コンポーネント内では、 this.props.todos で取得できる
// 子コンポーネント内では、props.todosで取得できる(要確認)
const mapStateToProps = state => ({
todos : state.todos
})
つまり、reduxのstoreに対するアクセスは、
- mapStateToPropsを利用してコンポーネント内でStoreから読み込みたいstateを定義。
- react-reduxのconnect関数を利用してReduxのデータにアクセスする。
ReactHooksに対応したReactのバージョンの場合は、react-reduxの「useSelector関数」を利用できる。
これは、react-redux v7.1.0でhook対応として「useDispatchとuseSelector」が利用できるようになったもの。
ちなみにReact自体は16.8.XでHooksに対応しているため、React自体と周辺ライブラリのアップデートも必要。
StoreのデータをUIのイベントから変更する方法
Reducerの目的は「現在の状態であるState」と「Action」を受け取り、「Actionで支持された内容」でStateに変更を加えて新しい状態を作ること。
制約として、reducer関数の中でのみstoreに保存されたデータの変更が可能。
Action自体はtypeプロパティを持ったJavaScriptのオブジェクトであり、Action自体がなにか処理を行うことはない。
// NOTE: Action
{
type: 'INCREASE_COUNT',
payload: payload
}
他にAction CreatorsというActionを作成する関数が存在することもある。
これは実行するとActionのオブジェクトを返却するものです。
// NOTE: Action Creators
export function increase(payload){
return {
type: 'INCREASE_COUNT',
payload: payload
}
}
reducerですが、stateとactionを引数に取ることができ、
受け取ったactionの内容に応じて、変更が加わったstateを返却します。
// Reducer
const reducer = (state = initState, action) => {
switch(action.type) {
case 'INCREASE_COUNT':
return { count: state.count +1 };
default:
return state;
}
}
このActionをreducerに伝える方法として、dispatch関数が存在します。
dispatch関数には引数としてActionを指定することができ、下記のように記述します。
// NOTE: dispatch
dispatch({type: INCREASE_COUNT});
これをメソッド内で実行するとreducerに実行したいActionが伝わり、
Store内のStateを変更できます。
import React from "react";
import { connect } from "react-redux";
// NOTE: 非Hooks - Connect関数 & dispatch処理を関数定義で実装した場合
function CountConnectDispatch({ dispatch, count }) {
const increment = () => {
dispatch({ type: "INCREMENT_COUNT" });
};
const decrement = () => {
dispatch({ type: "DECREMENT_COUNT" });
};
return (
<>
<div>Countコンポーネント:{count}</div>
<hr />
<button onClick={increment}>Up</button>
<button onClick={decrement}>Down</button>
</>
);
}
const mapStateToProps = (state) => {
return {
count: state.countReducer.count
};
};
export default connect(mapStateToProps)(CountConnectDispatch);
Hooksと非Hooksでいくつか記述方法があるため、五月雨にて記載。
import React from "react";
import { connect } from "react-redux";
// NOTE: 非Hooks - react-reduxが提供する「mapDispatchToProps」を利用した場合
function CountConnect({ increment, decrement, count }) {
return (
<>
<div>Countコンポーネント:{count}</div>
<hr />
<button onClick={increment}>Up</button>
<button onClick={decrement}>Down</button>
</>
);
}
const mapStateToProps = (state) => {
return {
count: state.countReducer.count
};
};
// IMO : dispatch処理の記載箇所がコンポーネント外になり、すこし見やすくなる。
const mapDispatchToProps = (dispatch) => {
return {
increment: () => dispatch({ type: "INCREMENT_COUNT" }),
decrement: () => dispatch({ type: "DECREMENT_COUNT" }),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(CountConnect);
import React from "react";
import { useSelector, useDispatch } from "react-redux";
// NOTE: Hooksで実現した場合
// IMO : 色々と簡素化されて記載量がとても減っている
function Count() {
const count = useSelector((state) => state.countReducer.count);
const dispatch = useDispatch();
const increment = () => {
dispatch({ type: "INCREMENT_COUNT" });
};
const decrement = () => {
dispatch({ type: "DECREMENT_COUNT" });
};
return (
<>
<div>Countコンポーネント:{count}</div>
<button onClick={increment}>Up</button>
<button onClick={decrement}>Down</button>
</>
);
}
export default Count;
ちなみにStoreのコードはこんな感じです。
// NOTE: store/index.js
import { createStore } from "redux";
const initState = {
count: 0,
};
const countReducer = (state = initState, action) => {
switch (action.type) {
case "INCREMENT_COUNT":
return {
count: state.count + 1,
};
case "DECREMENT_COUNT":
return {
count: state.count - 1,
};
default:
return state;
}
};
const store = createStore(countReducer);
console.log(store.getState());
export default store;
以上。