React redux-redux-toolkitで確認ポップアップを実装する方法
まえがき
モーダルポップアップを表示させ、その結果によって処理を変えたい場合があると思います。
confirmメソッドの ように簡単にどこからでも呼べて便利なポップアップを実装方法を共有したいと思います。
もし、改善点などございましたら、コメント・連絡いただけますと幸いです。
実装例
javascriptのconfirm
メソッドと同じように呼び出し、使用することを目的とします。
サンプルコード
ディレクトリ構造
├──components
| ├──functions
| | └──useConfirmationModalManagement.js
| └──pages
| └──Test.jsx
└──stores
├──Popup.js
└──index.js
Test.js
import React from "react";
import Button from "@mui/material/Button";
import { SetPopupState } from "~slices/Popup";
import { SendAPI } from "~slices/SendAPI";
import { useDispatch, useSelector } from "react-redux";
const Test = () => {
const dispatch = useDispatch();
const HandleClick = async () => {
const result = await open();
result && dispatch(SendAPI());//確認後に実行したい処理
};
return (
<React.Fragment>
<Button onClick={HandleClick}>送信</Button>
</React.Fragment>
);
};
export default Test;
useConfirmationModalManagement.js
import { useDispatch, useSelector } from "react-redux";
import { confirmationModalThunkActions, confirmationModalActions } from "~slices/Popup";
function useConfirmationModalManagement() {
const PopupState = useSelector((state) => state.Popup);
const dispatch = useDispatch();
const isOpened = PopupState.popup_open;
const open = async () => {
const { payload } = await dispatch(confirmationModalThunkActions.open());
return payload;
};
const confirm = () => {
return dispatch(confirmationModalActions.confirm());
};
const decline = () => {
return dispatch(confirmationModalActions.decline());
};
return {
isOpened,
open,
confirm,
decline,
};
}
export default useConfirmationModalManagement;
Popup.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
const initialState = {
isOpened: false,
isConfirmed: false,
isDeclined: false,
};
export const confirmationModalThunkActions = {
open: createAsyncThunk("Popup", async (arg, { extra, dispatch }) => {
dispatch(confirmationModalActions.open());
return new Promise((resolve) => {
const store = extra.store;
const unsubscribe = store.subscribe(() => {
const state = store.getState();
if (state.Popup.isConfirmed) {
console.log("isConfirmed");
unsubscribe();
resolve(true);
}
if (state.Popup.isDeclined) {
console.log("isDeclined");
unsubscribe();
resolve(false);
}
});
});
}),
};
// Sliceを生成する
const slice = createSlice({
name: "Popup",
initialState,
reducers: {
open: (state) => {
state.isOpened = true;
state.isDeclined = false;
state.isConfirmed = false;
},
confirm: (state) => {
state.isOpened = false;
state.isConfirmed = true;
},
decline: (state) => {
state.isOpened = false;
state.isDeclined = true;
},
}
});
// Reducerをエクスポートする
export default slice.reducer;
export const confirmationModalActions = slice.actions;
今回はextra, dispatchしか使用しないので、上記の書き方にしていますが、下記のように書き換えることも可能です。
createAsyncThunk("Popup", async (arg, thunkAPI) => {
const { extra, dispatch } = thunkAPI;
console.log(thunkAPI);
})
thunkAPIを確認すると下記の値が入っています。
- dispatch
- extra
- fulfillWithValue
- getState
- rejectWithValue
- requestId
- signal
getStateでstateを確認できますが、更新を取得できないため、extraに情報を入れ、stateを直接確認し、変更を検知するReduxのsubscribe
メソッドを使用します。
index.js
import { combineReducers } from "redux";
import { Store, configureStore } from "@reduxjs/toolkit";
import Popup from "./slices/Popup";
const reducer = combineReducers({
Popup: Popup,
});
const thunkArguments = {};
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
thunk: {
extraArgument: thunkArguments,
},
}),
});
thunkArguments.store = store;
export default store;
extraArgumentにstoreを追加することで、extraに任意の情報を付与することができます。
上記のようにすると、extraのstoreに下記の情報が付与されるはずです。
- dispatch: ƒ dispatch()
- getState: ƒ i()
- liftedStore: {dispatch: ƒ, subscribe: ƒ, getState: ƒ, replaceReducer: ƒ, Symbol(observable): ƒ}
- replaceReducer: ƒ replaceReducer(r)
- subscribe: ƒ subscribe(listener)
- Symbol
このsubscribe
メソッドを使用し、stateの変更を感知し、特定の値に変更された場合、非同期処理を終了するようにします。
その他のアプローチ
window.confirmメソッドを使用する方法
一番カンタンなのは、confirm
メソッドを使用することだけど、見た目がよろしくありません。
サンプルコード
import React from "react";
import Button from "@mui/material/Button";
import { SendAPI } from "~slices/SendAPI";
const Test = () => {
const dispatch = useDispatch();
const HandleClick = () => {
const result = confirm("送信しますか?");
if (!result) return;
dispatch(SendAPI());//確認後に実行したい処理
};
return (
<React.Fragment>
<Button onClick={HandleClick}>送信</Button>
</React.Fragment>
);
};
export default Test;
useEffectを使用する方法
useEffectで特定のstateに特定の値が入った時、処理を実行するように書くことで、実装可能ですが、呼び出し側に毎回useEffectの処理を記載する必要があり、 引数の渡し方も一度stateに保存する必要があります。
引数がなかったり、多くのページで共通して使用するものではない場合、この方法はとても良いと思います。
コールバックをストアに保存する方法(非推奨)
公式には推奨されていませんが、現状動作します。しかし、下記のエラーが発生してしまします。
A non-serializable value was detected in an action, in the path:
サンプルコード
確認画面を表示させる画面
import React from "react";
import Button from "@mui/material/Button";
import { SetPopupState } from "~slices/Popup";
import { useDispatch, useSelector } from "react-redux";
const Test = () => {
const dispatch = useDispatch();
const HandleClick = () => {
dispatch(PopupState({//stateを更新しポップアップを表示
isOpened:true,
send:() => dispatch(SendAPI()),//確認後に実行したい処理
}))
};
return (
<React.Fragment>
<Button onClick={HandleClick}>送信</Button>
</React.Fragment>
);
};
export default Test;
確認ポップアップの画面
import { SetPopupState } from "~slices/Popup";
import { useDispatch, useSelector } from "react-redux";
import { SendAPI } from "~slices/SendAPI";
const Popup = () => {
const PopupState = useSelector((state) => state.Popup);
const dispatch = useDispatch();
const handleClose = () => {
dispatch(
SetPopupState({
isOpened: false,
send: "",
})
);
};
const confirm = () => {
PopupState.send();
};
const decline = () => {
dispatch(
SetPopupState({
isOpened: false,
send: "",
})
);
};
return (
<React.Fragment>
<Dialog onClose={handleClose} open={PopupState?.isOpened}>
test
<Button onClick={confirm}>confirm</Button>
<Button onClick={decline}>decline</Button>
</Dialog>
{/* </Grid>
</Modal> */}
</React.Fragment>
);
};
エラーを非表示にする方法
storeを定義しているファイル。一般的にはindex.js
のミドルウェアに下記の値を設定することで、シリアル化できるかのチェックを外すことができます。
import { combineReducers } from "redux";
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export default store;