Reduxとは
Reactとは別の状態管理ライブラリです。Reactと一緒に使用されることが多いので、Reactの機能の一つだと思いがちですが、状態管理だけを担当するライブラリです🦾
なので他のjsライブラリと使用することもできます。
Reduxは難しい内容が多く、今はわからない部分はそういうお作法なんだと思った方がいいかもしれません..。
自分はそうしてます🙇♂️
一応、下記からRedux Application Data Flowでアプリ間でのデータの流れは確認できます。
ReduxのReducerには副作用は書けません。Reducerは純粋関数である必要があります。
純粋関数とは
- 関数の戻り値が引数のみに依存する
- 外部スコープのデータは参照・変更しない
- 引数で渡された値を変更しない
- 関数外に影響を及ぼさない
上記要件を満たさない操作は副作用と呼ばれ、副作用のある関数は純粋関数ではない。
使用例
下記のカウントアップコンポーネントを作成します。
useContextやuseReducerを理解している必要があります。
下記公式に従ってインストールします。
ディレクトリ構造は下記です。
├ component/
├ Counter.js
├ CounterButton.js
└ CounterResult.js
├ store/
├ index.js
├ modules/
└ counter.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
属性で管理します。
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です。
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()
を格納した変数counter
のactions
というプロパティから取得でき、それを分割代入とexportしています。
counter
のname
プロパティ、reducers
プロパティのメソッド名から自動でアクションのtypeプロパティが生成され、それに紐づいたreducerが実行されます。
最後にexport default counter.reducer;
していますが、configureStore()
の引数の中のreducer
プロパティに渡すreducer
として、createSlice()
を格納した変数counterのreducerプロパティをexportしています。
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()の処理内容を切り分けています。
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
を含むオブジェクトを返します。
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 👉
CounterResult
、CounterButton
コンポーネントを実行。 アクション用にprops
している。 - /components/CounterButton.js 👉
useDispatch()
でdispatch
の設定と、実行。設定したreducer
の処理が行われる - /components/CounterResult.js 👉
useSelector()
でグローバル管理しているstate
の取得と、表示。
グローバル管理したいstateがオブジェクトだった場合
管理しているstateはプリミティブ型でしたがオブジェクトだった場合の書き方です。
Reduxのreducer
も基本的にはuseReducer
と同様に、純粋関数であることとイミュータビリティの保持をする必要があります。
今回において、イミュータビリティの保持とは簡単にいうと、オブジェクトをコピーして、そのコピーした新しいオブジェクトの値を変えて返すことです。
イミュータブル
書き換えが不可(元の値は変わらない)。
文字列や数値、真偽値、BigInt、undefined、シンボル
ミュータブル
書き換えが可能(元の値が変わる)。
イミュータブルな値以外。オブジェクトや配列。
実際にstate
をオブジェクトにして試します。
下記のようにコードを変更します。
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レンダリング部分も変更します。
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では許可されます。
実際にコードを書き換えてみます。
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
を書いてはいけません。