【React】モーダルの開閉をRedux Sliceで状態管理する
本記事の内容
Reactでモーダルの開閉を状態管理する方法について記載しています。
モーダルの開閉についての実装は次の①、②の方法があります。
- ① 親コンポーネント/子コンポーネント間でモーダルの開閉フラグをstateを受け渡す方法
- ② Reduxでモーダルの開閉フラグを状態管理する方法
Reduxを用いた②の方法が断然楽! & Reduxの実装内容について、という内容です。
前提
コンポーネント
図のようなコンポーネントを実装をしているとします。
-
NewMatrixFlowPage
コンポーネント -
MatrixView
コンポーネント-
MatrixRow
コンポーネント
-
-
ModalforAddFlowstepForm
コンポーネント-
AddFlowstepForm
コンポーネント
-
fig1 : 実装コンポーネント
実装したい機能
NewMatrixFlowPage
コンポーネントはページのviewファイルです。
このNewMatrixFlowPage
上にMatrixView
コンポーネントを表示しています。
MatrixView
コンポーネントの子コンポーネントであるMatrixRow
コンポーネント上の+ボタンを押すと、モーダル(ModalforAddFlowstepForm
)が開く機能を実装したいという状況です。
コンポーネントの設計・機能について
このコンポーネントの設計は自分のポートフォリオ「MATRIXFLOW」のものです。
「MATRIXFLOW」についてはこちらをご覧ください
親/子コンポーネント間でstateの受け渡しはつらい
モーダルを開くだけなのですが、先述のコンポーネントらにモーダルを開くためのフラグ(ModalOpen
とします)を受け渡そうとすると...
- 子コンポーネント → 親コンポーネントへの受け渡し
-
MatrixRow
コンポーネント →MatrixView
コンポーネント -
MatrixView
コンポーネント →NewMatrixFlowPage
コンポーネント
-
- 親コンポーネント → 子コンポーネントへの受け渡し
-
NewMatrixFlowPage
コンポーネント →ModalforAddFlowstepForm
コンポーネント
-
3回の受け渡しが必要になります。しかも、親→子 / 子→親の場合分けが出てきます。
これはつらい。。
fig2 : 親/子コンポーネント間でstateの受け渡し (つらい)
Reduxを使おう
store現る
先ほどの煩雑な親↔︎子の状態の受け渡しを回避したい、そこでReduxです。
Reduxのイメージは、
「store
という絶対的親を作って、状態を共有化する」
です。
先ほどの状態の受け渡しは次のようになります。
fig3 : storeを使った場合の仮イメージ図
実際にやり取りされているのは「指令」
fig3はfig2と対比するようにしてモーダル開フラグModalOpen
を受け渡ししているように描きましたが、正しくないので「仮イメージ図」としています。
Reduxを使った場合、実際にやり取りするのはモーダル開状態フラグModalOpen
ではなく、「store
の状態を変更・参照する指令
」です。
モーダル開状態フラグを保持しているのはstore
で、store
から動きません。
fig4のようになります。
- storeの状態を変更
- storeに状態(state)を定義
- storeの状態を変更する指令(action)をstoreで定義
-
MatrixRow
コンポーネントからactionを実行する(dispatch
)
- storeの状態を参照
- storeに状態(state)を定義
-
ModalforAddFlowstepForm
コンポーネントからstateを参照する指令(useSelector
)により、モーダル開フラグを受け取る - モーダル開フラグの値( true / false )に基づいてモーダルを表示する/非表示にする
fig4 : storeを使った場合のstateの受け渡し
Redux Sliceの実装内容
Reduxの実装内容について記していきます。
エントリーファイルにstoreの設定を読み込むよう設定
app.jsx
のようなエントリーファイルにstoreの設定を読む込むように設定します。
import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import store from '/path/to/store'; //store.jsへのパスを設定
createInertiaApp({
setup({ el, App, props }) {
const root = createRoot(el);
root.render(
<>
<Provider store={store}>
<App {...props} />
</Provider>
</>
);
},
});
Sliceファイルの作成
ReduxはState
, Reducer
, Action
の3要素からなり別々のファイルで記述する方法がありますが、Slice
を使うと1つのファイルにそれらをまとめて記述することが便利です。
// Sliceをインポート
import { createSlice } from '@reduxjs/toolkit';
// 初期状態を定義
const initialState = {
ModalOpen: false,
};
// モーダルの開閉状態に関するRedux Sliceを定義
const modalSlice = createSlice({
// 他のコンポーネントで呼び出す時の名前を定義
name: 'modal',
// 初期状態
initialState,
// Reducer(Actionを定義する場所)
reducers: {
// モーダルを開くAction
openModal: (state) => {
state.ModalOpen = true;
},
// モーダルを閉じるAction
closeModal: (state) => {
state.ModalOpen = false;
},
},
});
// 定義したActionを他のコンポーネントで呼び出せるようにexport設定
export const {
openModal,
closeModal,
} = modalSlice.actions;
// storeに登録できるようにReducerをexport設定
export default modalSlice.reducer;
Sliceをstoreに登録
storeに定義したSliceファイルを登録します。
import { configureStore } from '@reduxjs/toolkit';
import modalReducer from 'path/to/modalSlice'; //modalSliceへのパスを設定
const store = configureStore({
reducer: {
modal: modalReducer,
},
});
export default store;
Redux Sliceの状態を変更する(Actionをdispatch)
storeで定義されたstate:ModalOpen
を変更するアクションをopenModal
として定義したので、モーダルを開くボタンをクリックしたときにそのアクションが実行されるように実装します。
他のコンポーネントからstoreに定義されたアクションを呼び出して使うにはdispatch
を使います。
import React from 'react';
// Reduxのdispatchを使用できるように設定
import { useDispatch } from 'react-redux';
// FontAwesomeのアイコンのインポート
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSquarePlus } from '@fortawesome/free-solid-svg-icons';
// Store Redux Sliceのインポート
import { openModal } from '/path/to/modalSlice'; //modalSliceへのパスを設定
const MatrixCol = () => {
// ...省略
};
const MatrixRow = ({ member }) => {
const dispatch = useDispatch();
const handleOpenModal = () => {
dispatch(openModal());
};
return (
<tr>
<td>
<div>
<div>
{member.name}
</div>
</div>
</td>
{Array.from({ length: maxFlowNumber }, (_, i) => i + 1).map((flowNumber) => {
return (
<React.Fragment key={flowNumber}>
<MatrixCol/>
</React.Fragment>
);
})}
<td>
<button onClick={handleOpenAddFlowstepModal}>
<FontAwesomeIcon icon={faSquarePlus} />
</button>
</td>
</tr>
);
};
const MatrixView = () => {
return (
<table>
<thead>
<tr>
<th>担当者 / フローステップ</th>
// ...省略
</tr>
</thead>
<tbody>
{members.map((member, index) => (
<MatrixRow/>
))}
// ...省略
);
};
export default MatrixView;
Redux Sliceの状態を参照する(StateをuseSelectorで参照)
storeの状態を参照するにはuseSelector
を使います。
useSelectore
で参照する状態の値を使ってモーダルの表示を制限する例です。
import React from 'react';
// useSelectorを使えるようにインポート設定
import { useSelector } from 'react-redux';
// コンポーネントの読み込み
import ModalforAddFlowstepForm from '/path/to/ModalforAddFlowstepForm';
const NewMatrixFlowPage = () => {
// useSelectorによりstoreに定義したStateを参照
const ModalOpen = useSelector((state) => state.modal.ModalOpen);
return (
<div>
// useSelectorで参照したModalOpenがtrueの時に表示する
{ModalOpen && (
<ModalforAddFlowstepForm>
<AddFlowstepForm/>
</ModalforAddFlowstepForm>
)}
</div>
);
};
export default NewMatrixFlowPage;
終わりに
親子間の状態の受け渡しが増えるとコードがカオスになるので早めにRedux使うのが吉と言う所感です
Discussion