最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Generic modals with Redux and Thunk - Stack Overflow

programmeradmin5浏览0评论

I've been looking into creating generic modals with React, Redux, and Thunk. Ideally, my state would look like the following:

export interface ConfirmModalState {
  isOpened: boolean;
  onConfirm: null | Function
}

export const initialConfirmModalState: ConfirmModalState = {
  isOpened: false,
  onConfirm: null
};

However, this would mean putting non-serializable data into the state, which seems to be highly discouraged.

I've read a great blogpost by markerikson. However, I don't think the proposed solution would work with asynchronous actions and Thunk.

How do you suggest to resolve this issue?

I've been looking into creating generic modals with React, Redux, and Thunk. Ideally, my state would look like the following:

export interface ConfirmModalState {
  isOpened: boolean;
  onConfirm: null | Function
}

export const initialConfirmModalState: ConfirmModalState = {
  isOpened: false,
  onConfirm: null
};

However, this would mean putting non-serializable data into the state, which seems to be highly discouraged.

I've read a great blogpost by markerikson. However, I don't think the proposed solution would work with asynchronous actions and Thunk.

How do you suggest to resolve this issue?

Share Improve this question asked Apr 22, 2021 at 12:32 Marcin WanagoMarcin Wanago 6537 silver badges13 bronze badges 1
  • what if you want different modals on different platforms? – webduvet Commented Apr 23, 2021 at 14:34
Add a ment  | 

2 Answers 2

Reset to default 10

I actually wrote the post that you linked, and I wrote a much-expanded version of that post a couple years later:

Practical Redux, Part 10: Managing Modals and Context Menus.

I've actually implemented a couple variations of this approach myself since I wrote that post, and the best solution I've found is to add a custom middleware that returns a promise when you dispatch a "show modal" action, and resolves the promise with a "return value" when the dialog is closed.

There's an existing implementation of this approach at https://github./AKolodeev/redux-promising-modals . I ended up making my own implementation. I have a partial version of my homegrown approach in a gist at https://gist.github./markerikson/8cd881db21a7d2a2011de9e317007580 , and the middleware looked roughly like:

export const dialogPromiseMiddleware: Middleware<DialogPromiseDispatch> = storeAPI => {
    const dialogPromiseResolvers: Record<string, Resolver> = {};

    return next => (action: AnyAction) => {
        switch (action.type) {
            // Had to resort to `toString()` here due to https://github./reduxjs/redux-starter-kit/issues/157
            case showDialogInternal.toString(): {
                next(action);
                let promiseResolve: Resolver;
                const dialogPromise = new Promise((resolve: Resolver) => {
                    promiseResolve = resolve;
                });

                dialogPromiseResolvers[action.payload.id] = promiseResolve!;

                return dialogPromise;
            }
            case closeDialog.toString(): {
                next(action);
                const {id, values} = action.payload;
                const resolver = dialogPromiseResolvers[id];
                if (resolver) {
                    resolver(values);
                }

                delete dialogPromiseResolvers[id];
                break;
            }
            default:
                return next(action);
        }
    };
};

(note: I made that gist when I was having some TS syntax issues getting dispatching to work correctly, so it's likely it won't 100% work out of the box. RTK also now includes some .match() action matching utilities that would be useful here. but, it shows the basic approach.)

The rough usage in a ponent is:

const closedPromise = dispatch(showDialog("TestDialog", {dialogNumber : counter});
const result = await closedPromise
// do something with the result

That way you can write the "on confirm" logic write there in the place that asked for the dialog to be shown in the first place.

Thank you markerikson for providing an answer. This inspired me to create a solution with thunks. Please give me some feedback here :) I will be using hooks and @reduxjs/toolkit in my example.

This is the state of my ConfirmationModal reducer:

export interface confirmationModalState {
  isOpened: boolean;
  isConfirmed: boolean;
  isCancelled: boolean;
}

export const initialConfirmationModalState: confirmationModalState = {
  isOpened: false,
  isConfirmed: false,
  isCancelled: false,
};

This is the slice (a bination of the reducer and actions):

import { createSlice } from '@reduxjs/toolkit';
import { initialConfirmationModalState } from './state';

const confirmationModalSlice = createSlice({
  name: 'controls/confirmationModal',
  initialState: initialConfirmationModalState,
  reducers: {
    open: state => {
      state.isOpened = true;
      state.isConfirmed = false;
      state.isCancelled = false;
    },
    confirm: state => {
      state.isConfirmed = true;
      state.isOpened = false;
    },
    cancel: state => {
      state.isCancelled = true;
      state.isOpened = false;
    },
  },
});

export const confirmationModalActions = confirmationModalSlice.actions;

export default confirmationModalSlice;

This is the thunk action for it:

import { createAsyncThunk } from '@reduxjs/toolkit';
import ThunkApiConfig from '../../../types/ThunkApiConfig';
import { AppState } from '../../reducers';
import { confirmationModalActions } from './slice';

const confirmationModalThunkActions = {
  open: createAsyncThunk<boolean, void, ThunkApiConfig>(
    'controls/confirmationModal',
    async (_, { extra, dispatch }) => {
      const store = extra.store;

      dispatch(confirmationModalActions.open());

      return await new Promise<boolean>(resolve => {
        store.subscribe(() => {
          const state: AppState = store.getState();
          if (state.controls.confirmationModal.isConfirmed) {
            resolve(true);
          }
          if (state.controls.confirmationModal.isCancelled) {
            resolve(false);
          }
        });
      });
    },
  ),
};

export default confirmationModalThunkActions;

You can notice it uses extra.store to perform the subscribe. We need to provide it when creating a store:

import binedReducers from './reducers';
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { ThunkExtraArguments } from '../types/ThunkExtraArguments';

function createStore() {
  const thunkExtraArguments = {} as ThunkExtraArguments;

  const customizedMiddleware = getDefaultMiddleware({
    thunk: {
      extraArgument: thunkExtraArguments,
    },
  });

  const store = configureStore({
    reducer: binedReducers,
    middleware: customizedMiddleware,
  });
  
  thunkExtraArguments.store = store;

  return store;
}

export default createStore();

Now, let's create a hook that allows us to dispatch all of the above actions:

import { useDispatch, useSelector } from 'react-redux';
import { AppState } from '../../../reducers';
import { useCallback } from 'react';
import confirmationModalThunkActions from '../thunk';
import { confirmationModalActions } from '../slice';
import { AppDispatch } from '../../../../index';

export function useConfirmationModalState() {
  const dispatch: AppDispatch = useDispatch();
  const { isOpened } = useSelector((state: AppState) => ({
    isOpened: state.controls.confirmationModal.isOpened,
  }));

  const open = useCallback(() => {
    return dispatch(confirmationModalThunkActions.open());
  }, [dispatch]);

  const confirm = useCallback(() => {
    dispatch(confirmationModalActions.confirm());
  }, [dispatch]);

  const cancel = useCallback(() => {
    dispatch(confirmationModalActions.cancel());
  }, [dispatch]);

  return {
    open,
    confirm,
    cancel,
    isOpened,
  };
}

(don't forget to attach confirm and cancel to the buttons in your modal)

And that's it! We can now dispatch our confirmation modal:

export function usePostControls() {
  const { deleteCurrentPost } = usePostsManagement();
  const { open } = useConfirmationModalState();

  const handleDelete = async () => {
    const { payload: isConfirmed } = await open();

    if (isConfirmed) {
      deleteCurrentPost();
    }
  };

  return {
    handleDelete,
  };
}
发布评论

评论列表(0)

  1. 暂无评论