1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ReactにおけるRedux Toolkitの使い方 ✍🏻

Posted at

Reduxとは

Reactとは別の状態管理ライブラリです。Reactと一緒に使用されることが多いので、Reactの機能の一つだと思いがちですが、状態管理だけを担当するライブラリです🦾
なので他のjsライブラリと使用することもできます。
Reduxは難しい内容が多く、今はわからない部分はそういうお作法なんだと思った方がいいかもしれません..。
自分はそうしてます🙇‍♂️
一応、下記からRedux Application Data Flowでアプリ間でのデータの流れは確認できます。

ReduxのReducerには副作用は書けません。Reducerは純粋関数である必要があります。

純粋関数とは

  • 関数の戻り値が引数のみに依存する
  • 外部スコープのデータは参照・変更しない
  • 引数で渡された値を変更しない
  • 関数外に影響を及ぼさない

上記要件を満たさない操作は副作用と呼ばれ、副作用のある関数は純粋関数ではない。

使用例

下記のカウントアップコンポーネントを作成します。

スクリーンショット 2023-01-27 22.48.15.png

useContextやuseReducerを理解している必要があります。

下記公式に従ってインストールします。

ディレクトリ構造は下記です。

├ component/
  ├ Counter.js
  ├ CounterButton.js
  └ CounterResult.js
├ store/
  ├ index.js
  ├ modules/
     └ counter.js

└ Example.js

Example.js
import Counter from "./components/Counter";
import { Provider } from "react-redux";
import store from "./store"

const Example = () => {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
};

export default Example;

useContextでグローバルな状態管理をする際にはProviderというコンポーネントを使用し、そのvalueにセットされた値がネストされたコンポーネントで取得することができるようなりますよね。
それと似たようなものです🙌
reduxではvalueではなくstore属性で管理します。

store / index.js
import { configureStore } from "@reduxjs/toolkit";
import reducer from "./modules/counter";
 // configureStore()はStoreを定義する関数。
 // reducer名と作成したreducerを渡す。
export default configureStore({
  reducer: {
    counter: reducer,
  },
});

そのstore属性に渡すグローバルで状態管理したい中身です。
configureStore()を使用しStoreを定義します。stateを格納し参照したりしておく保管庫のようなものですかね..🚪
configureStore()は引数にオブジェクトを取ります。
そのオブジェクトにreducerというプロパティを定義し、reducer名と作成したreducer(更新用関数)を設定しておきます。
渡しているreducerは下記のcounter.jsでexportしているreducerです。

store / modules / counter.js
import { createSlice } from "@reduxjs/toolkit";

 // createSlice()はreducerを定義する関数。
 // グローバルで管理したいstateとreducerの処理内容(stateの値をどう更新するか)を書いていく。
const counter = createSlice({
  name: "counter", // このSliceの名前
  initialState: 0, // state初期値
  reducers: {
    // 定義したいメソッド群。このメソッドの引数にstateとアクションが渡ってくる
    add(state, { payload }) {
      return state + payload;
    },
    minus(state, { payload }) {
      return state - payload;
    },
  },
});


 // Actions Create(アクション部分の関数)はtoolkitを使用した場合自動で生成
const { add, minus } = counter.actions;
export { add, minus };

// configureStore()に渡すreducerとして、変数counterのreducerプロパティを export
export default counter.reducer;

createSlice()はreducerを定義する関数です。
こちらも引数にオブジェクトをとり、グローバルで管理したいstateとreducerの処理内容(stateの値をどう更新するか)を書いています。
toolkitを使用した場合、Actions Create(アクション部分の関数)は自動で生成されます🎉!
createSlice()を格納した変数counteractionsというプロパティから取得でき、それを分割代入とexportしています。
counternameプロパティ、reducersプロパティのメソッド名から自動でアクションのtypeプロパティが生成され、それに紐づいたreducerが実行されます。
最後にexport default counter.reducer;していますが、configureStore()の引数の中のreducerプロパティに渡すreducerとして、createSlice()を格納した変数counterのreducerプロパティをexportしています。

components / Counter.js
import CounterResult from "./CounterResult"
import CounterButton from "./CounterButton"

const Counter = () => {
    return (
        <>
            <CounterResult />
            <CounterButton step={2} calcType="+"/>
            <CounterButton step={2} calcType="-"/>
            <CounterButton step={10} calcType="+"/>
            <CounterButton step={10} calcType="-"/>
        </>
    )
}
export default Counter;

propsで値を渡しています。この値をアクションに使用して、reducer()の処理内容を切り分けています。

components / CounterButton.js
import { useDispatch } from "react-redux";
import { add, minus } from "../store/modules/counter"

const CounterButton = ({calcType, step}) => {
 
    // useDispatch()で、createSlice()の引数に渡したreducer取得
    const dispatch = useDispatch();
    
    const clickHandler = () => {
        const action = calcType === '+' ? add(step) : minus(step);
        dispatch(action);
    }

    return <button onClick={clickHandler}>{calcType}{step}</button>
}
export default CounterButton;

useDispatch()で、createSlice()の引数に渡したreducersが取得できます。
取得したdispatch()の引数にアクションを渡し、reducerを切り替えています。
add(step)minus(step)は自動で生成されているActions Createです✏️
実行時にtypeを含むオブジェクトを返します。

components / CounterResult.js
import { useSelector } from "react-redux";
const CounterResult = () => {
  const state = useSelector((state) => state);
  return <h3>{state.counter}</h3>;
};

export default CounterResult;

useSelector()でグローバルで管理しているstateが取得できます。
コールバック関数を持つ必要があり、今回はそのまま返しています。

これでpropsでのバケツリレーをすることなく、様々なコンポーネント間でデータのやりとりができているかと思います!🚀

処理のまとめ 📌

  • /Example.js 👉 Providerの使用とstoreの設定。
  • /store/index.js 👉 configureStore()を使用してStore定義。reducerの設定。
  • /store/modules/counter.js 👉 createSlice()reducer定義。グローバル状態管理するstate、更新関数処理、アクションを記述。
  • /components/Counter.js 👉 CounterResultCounterButtonコンポーネントを実行。 アクション用にpropsしている。
  • /components/CounterButton.js 👉 useDispatch()dispatchの設定と、実行。設定したreducerの処理が行われる
  • /components/CounterResult.js 👉 useSelector()でグローバル管理しているstateの取得と、表示。

グローバル管理したいstateがオブジェクトだった場合

管理しているstateはプリミティブ型でしたがオブジェクトだった場合の書き方です。
Reduxのreducerも基本的にはuseReducerと同様に、純粋関数であることとイミュータビリティの保持をする必要があります。
今回において、イミュータビリティの保持とは簡単にいうと、オブジェクトをコピーして、そのコピーした新しいオブジェクトの値を変えて返すことです。

イミュータブル
書き換えが不可(元の値は変わらない)。
文字列や数値、真偽値、BigInt、undefined、シンボル

ミュータブル
書き換えが可能(元の値が変わる)。
イミュータブルな値以外。オブジェクトや配列。

実際にstateをオブジェクトにして試します。
下記のようにコードを変更します。

store / modules / counter.js
import { createSlice } from "@reduxjs/toolkit";

const counter = createSlice({
  name: "counter", 
- initialState: 0, 
+ initialState: {
+   count: 0,
+ },
  reducers: {
    add(state, { payload }) {
-     return state + payload;
+     const newState = { ...state }; // stateを展開して新しいオブジェクト作成
+     newState.count = state.count + payload;
+     return newState;
    },
    minus(state, { payload }) {
-     return state - payload;
+     const newState = { ...state }; 
+     newState.count = state.count - payload;
+     return newState;
    },
  },
});


const { add, minus } = counter.actions;
export { add, minus };

export default counter.reducer;

stateレンダリング部分も変更します。

components / CounterResult.js
import { useSelector } from "react-redux";
const CounterResult = () => {
-  const state = useSelector((state) => state);
-  return <h3>{state.counter}</h3>;
+  const count = useSelector((state) => state.counter.count);
+  return <h3>{count}</h3>;
};

export default CounterResult;

こちらで実行してみると、stateがプリミティブ型だった時と同様に動作します🙌

Toolkitではミュータブルな変更が許容される

まず大前提として、先述した通り、Reduxのreducerに関しても純粋関数である必要があり、上記ではイミュータビリティを保持した書き方をしました。
ですが実は、Toolkitの中ではミュータブルな操作が、Immerというライブラリによって内部的にはイミュータブルに取り扱われるそうです..🤔

ミュータブルな操作とは引数で渡ってきたオブジェクトのプロパティを書き換えてしまう操作のことを指します。

純粋関数の中では基本的にこのような操作をしてはいけないのですが、Redux Toolkitでは許可されます。
実際にコードを書き換えてみます。

store / modules / counter.js
import { createSlice } from "@reduxjs/toolkit";

const counter = createSlice({
  name: "counter", 
  initialState: {
    count: 0,
  },
  reducers: {
    add(state, { payload }) {
-     const newState = { ...state };
-     newState.count = state.count + payload;
-     return newState;
+     state.count = state.count + payload;
    },
    minus(state, { payload }) {
-     const newState = { ...state };
-     newState.count = state.count - payload;
-     return newState;
+     state.count = state.count - payload;
    },
  },
});


const { add, minus } = counter.actions;
export { add, minus };

export default counter.reducer;

こちらで実行すると、追加したコードのようなミュータブルな操作をしても正常に動作することがわかります。
注意としてはこのようなミュータブルな変更を加えたオブジェクトを、 新しいstateにしたい場合はreturnを書いてはいけません。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?