カグラボ

<!--何かとごちゃごちゃしがちな思考を整理するブログ-->

考古学 : react-reduxを学ぶ

2022/06/26

概要

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に対するアクセスは、

  1. mapStateToPropsを利用してコンポーネント内でStoreから読み込みたいstateを定義。
  2. 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;

以上。