今回Reactのプロジェクトで、Reduxを使用する機会があり、自分の備忘録も兼ねて、Reduxの基礎部分で学んだことをまとめてみました。
目次
Reduxってなに?
Reduxとは、flux
のアーキテクチャ概念に基づいて構成されている状態管理ライブラリです。
Reduxを使用することで、アプリケーション全体でデータを一元管理
することができ、コンポーネント間のデータのやり取りをスムーズに行う
ことができます。
AngularやJQueryなどのほかのJavaScriptライブラリと使用することも可能で、Reactで使用する場合は、Reduxライブラリに加えて、react-reduxのライブラリのインストールが必要となります。
fluxとは
ReactにおけるRedux使用のメリット
Reactでは親から子コンポーネントへstateを渡したい場合、バケツリレーのように上から順にデータを受け渡す必要がありました。親→孫→子の階層が深くなるほど、データの受け渡しは複雑になります。また、親子間を通さずに、別の階層のコンポーネントのstateを参照する操作も通常行うことができません。
Reduxを使用すると...
Reduxはstateを一元管理する保管場所のような「store」
を保持しているので、storeにあるstateは全コンポーネントから参照することができ、バケツリレーを経由せず、storeから直接stateを取得
することができます。
Redux - 3つの原則 -
1. 信頼できる唯一の情報源
・アプリケーションの状態は、単一のストアのオブジェクトツリーで保存されます。
1つのstoreで状態が管理されることによって、アプリケーションのデバッグや調査が容易になります
2. 状態は読み取り専用
・状態を変更する手段は、変更の情報を持つActionを発行する場合のみ
変更はデータフローに従って、順序立てて実行されるため、変更の衝突などがなくなります
3. 変更は純粋な関数で行われる
・変更は前の状態とアクションを取得して次の状態を返す純粋なReducer関数によって行われます。状態を更新する場合は、以前の状態の書き換えを行うのではなく、新しいオブジェクトを返すようにしなければいけません。
Redux構造
Reduxの基本のデータの流れは下記のようなイメージになります。
基本のデータの流れ
- ユーザーのクリックや入力等の操作があった場合、 ActionCreatorsによってActionが生成される
- 生成されたActionをStoreへ送信(Dispatch)する
- ReducerがActionを受け取り、Store内の既存stateと組み合わせて新しい状態に更新する
- UIは新しい状態を読み取り、更新された値を表示する
各役割を1つずつ細かくみていきます。
Action
Actionは、Actionの種類(type)
と変更に必要な付加情報(payload)
を持つオブジェクトです。
ユーザーが入力やクリックなどの操作を行うことで、Actionが生成されます。
{
type: "INCREASE",
payload: 2
};
通常、下記のようなActionオブジェクトを返すActionCreators関数
を作成して、Actionを呼び出します。
export const increase = () => {
return {
type: "INCREASE",
payload: 2
};
};
Dispatch
DispatchはActionを受け取って、storeへ送る役割を果たしています。
hooksのuseDispatch
を呼び出して、dispatch関数
を使用することができます。
import { useDispatch } from 'react-redux';
const dispatch = useDispatch();
const countUp = () => {
dispatch(increase()); // 引数でActionを受け取る
}; // ここでは1つ前で説明したActionCreatorsを呼び出しています)
<button onClick={countUp}>countUp</button>
Reducer
Reducerは、受け取ったActionの種類(type)
、オプションの情報(payload)
によって、stateを更新します。引数でstate(更新前)とactionを受け取り、state(更新後)を返します。
export const counterReducer = (state = 0, action) => {
switch (action.type) {
case "INCREASE":
return state + action.payload;
case "DECREASE";
return state - action.payload;
default;
return state;
}
};
- Reducerの特別なルール -
1. 以前のstateとactionオブジェクトに基づいて新しいstateのみを計算する。
2. 既存のstateを更新してはいけない。
代わりにstateをコピーし、コピーした値を更新することによって不変の更新
を行う必要がある。
// ❎ 直接の更新
state.value = 123;
// ✅ コピーした値の更新
return {
...state,
value: 123
}
3. 非同期ロジックやその他の「副作用」を実行してはいけない。
reducerは特定の入力が渡されたときに特定の値を返す純粋関数
である為、副作用の実行はできない
。
副作用例
コンソールログへの出力
DOM操作
サーバーとの通信
ランダムな値の生成
タイマー処理
副作用の処理は、MiddleWareに記載しましょう。
Store
Storeはstateを管理する1つの保管場所
のようなものです。
createStore
でStoreを作成することができます。
import { createStore } from "redux";
const store = createStore(counterReducer); // 作成したreducerをimport
reducerが複数の場合は、combineReducers
で1つに収集することで、storeで扱うことができます。
import { combineReducers } from "redux";
const reducers = combineReducers({
counter: reducer,
todo: todoReducer
});
export default reducers;
現在ではReduxToolkit
1を用いた実装が推奨されており、storeの作成は、configureStore
が使用されることが多いです。configureStoreの場合は、複数のReduceを扱う時も、自動でまとめてくれる為、combineReducersの使用は不要です。
// redux-toolkitの場合
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {
count: counterReducer,
todo: todoReducer
}
});
export default store;
- Storeの呼び出し -
Provider
をimportし、AppコンポーネントをProviderで囲い、作成したstore
を渡します。
Providerは、storeとstateを繋げてくれる役割
を果たしています。
import Counter from "./components/Counter";
import { Provider } from "react-redux";
import store from "./store";
const App = () => {
return (
<Provider store={store}>
<Counter />
</Provider>
);
};
export default App;
Selector
hooksのuseSelector
を使用することで、stateの値を取得
することができます。
state取得後、コンポーネントが再レンダリングされることで、ユーザーが更新された値を確認することができます。
import { useSelector } from "react-redux";
const Counter = () => {
const count = useSelector((state) => state.count);
return <div>{count}</div>;
};
export default Counter;
- Redux 非同期処理 -
Reduxで非同期処理を行う場合に、必要になってくるのがmiddleware
の存在です。
非同期処理を行う場合のRedux構造は下記のような図のイメージになります。
Middleware
非同期処理はstore内で実行することはできない為、非同期処理はstoreの外側で実行する必要があります。非同期処理と現在のstoreの状態を繋げてくれる役割を果たしているのが、middleWare
です。reduxで使用されているmiddlewareは多くありますが、最も使用されているものが、redux-thunk
です。
redux-thunk
ActionCreatorはActionオブジェクトを返しますが、redux-thunk
の場合は、thunk関数
を返します。
これによって、Actionのdispatchを遅らせることができたり、特定の条件の場合にdispatchを行うなどの処理が可能になります。thunk関数内では、引数のdispatch
とgetState
の使用ができます。
export const addAsync = (payload) => {
return async (dispatch, getState) => {
// stateの取得
const state = getState();
const response = await asyncCount(payload);
// dispatchの使用
dispatch(add(response.data));
};
};
コンポーネントによる記述の違い
現在では関数コンポーネントによる記述が多いですが、今回のプロジェクトではクラスコンポーネントによるReduxの記述だった為、2つの記述の違いについても触れたいと思います。
2つの記述の主な違い
1. stateを取得する
mapStateToProps() → useSelector()
2. Actionをdispatchする
mapDispatchToProps() → useDispatch()
3. componentとStoreを連携させる
connect() → 不要
stateの取得
クラスコンポーネントではmapStateToProps、関数コンポーネント+hooksでは、「useSelector()」を使用します。
- クラスコンポーネントの場合
import { getLanguage } from "./selectors";
class Sample extends Component {
componentDidMount() {
this.props.setLanguageAction(this.props.language);
}
render() {
// jsx
}
}
// mapStateToProps の追加
const mapStateToProps = (state) => {
return {
language: getLanguage(state),
};
};
- 関数コンポーネントの場合
import { useSelector } from "react-redux";
import { getLanguage } from "./selectors";
const Sample = () => {
const language = useSelector(getLanguage);
return(
//jsx
)
};
export default Sample;
Dispatch
クラスコンポーネントでは「mapDispatchToProps」、関数コンポーネント+hooksでは「useDispatch()」を使用することができます。
- クラスコンポーネントの場合
import { getLanguage } from "./selector";
import { setLanguageAction } from "./actions";
class Sample extends Component {
componentDidMount(){
this.props.setLanguageAction(this.props.language);
}
render() {
// jsx
}
}
// mapDispatchToProps の追加
const mapDispatchToProps = (dispatch) => {
//bindActionCreatorは通常のActionの呼び出しを簡潔にしてくれるもの
return bindActionCreators({ setLanguageAction }, dispatch);
};
- 関数コンポーネントの場合
import { useSelector, useDispatch } from "react-redux";
import { getLanguage } from "./selectors";
import { setLanguageAction } from "./actions";
const Sample = () => {
const language = useSelector(getSelectedLanguage);
const dispatch = useDispatch();
useEffect(() => {
dispatch(setLanguageAction(language));
}, []);
return (
//jsx
);
};
export default Sample;
componentとstoreの連携
クラスコンポーネントでは、storeとコンポーネントを接続する為にconnect関数
を使用します。関数コンポーネントの場合は不要です。
- connect -
connect関数にstate
とdispatch
をわたすことで、コンポーネントはこれらをprops
として受けることができます。storeの状態に変更があった場合はpropsのstateを自動で更新し、propsのdispatch関数が呼び出された時には、自動でdispatchを行ってくれます。
- クラスコンポーネント
import { getLanguage } from "./selector";
import { setLanguageAction } from "./actions";
class Sample extends Component {
componentDidMount(){
this.props.setLanguageAction(this.props.language);
}
render() {
// jsx
}
}
const mapStateToProps = (state) => {
return {
language: getLanguage(state),
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ setLanguageAction }, dispatch);
};
// storeとコンポーネントを接続
export default connect(mapStateToProps, mapDispatchToProps)(Sample);
ひとつ前の関数コンポーネントの場合と比較して分かるように、関数コンポーネント+hooksの使用で、より簡潔にReduxの記述を行うことができるようになりました。
最後に
超入門な記事になりましたが、最後まで見てくださり有難うございました!
2023年も残り僅か。。みなさま今年もお疲れ様でした
-
ReduxToolKit:https://redux-toolkit.js.org/
Redux-ToolKitとは、Reduxを用いた開発を効率的に行うためのツールキットです。 素のReduxでは機能が足りない部分があるため、Immerやredux-thunk,createSliceなどのライブラリが使用されますが、これらのライブラリを丸ごと同封しています。 ↩