【问题标题】:Generic modals with Redux and Thunk使用 Redux 和 Thunk 的通用模式
【发布时间】:2021-07-16 16:24:11
【问题描述】:

我一直在研究使用 React、Redux 和 Thunk 创建通用模式。理想情况下,我的状态如下所示:

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

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

但是,这意味着将不可序列化的数据放入状态,这似乎是非常不鼓励的。

我读过markeriksongreat blogpost。但是,我认为建议的解决方案不适用于异步操作和 Thunk。

您建议如何解决此问题?

【问题讨论】:

  • 如果你想在不同平台上使用不同的模态怎么办?

标签: javascript reactjs redux react-redux redux-thunk


【解决方案1】:

我实际上写了你链接的帖子,几年后我写了该帖子的扩展版本:

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

自从我写那篇文章以来,我自己实际上已经实现了这种方法的几个变体,我发现的最佳解决方案是添加一个自定义中间件,当您调度“显示模式”操作时返回一个承诺,并且当对话框关闭时,使用“返回值”解决承诺。

https://github.com/AKolodeev/redux-promising-modals 上已有这种方法的实现。我最终做出了自己的实现。我在https://gist.github.com/markerikson/8cd881db21a7d2a2011de9e317007580 的要点中有我自己开发的方法的部分版本,中间件大致如下:

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.com/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);
        }
    };
};

(注意:当我遇到一些 TS 语法问题以使调度正常工作时,我提出了这个要点,因此它可能不会 100% 开箱即用。RTK 现在还包括一些 .match() 动作匹配实用程序这在这里很有用。但是,它显示了基本方法。)

一个组件中的粗略用法是:

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

这样你就可以在最初要求显示对话框的地方写上“确认”逻辑。

【讨论】:

  • 非常感谢您的回答。我在下面的答案中提供了一种受您的代码启发的方法。我一定会很感激一些反馈。
  • 我只是好奇传统方法有什么问题 - 实现 onClose 并传递回调? UI 逻辑保留在 UI 中,您不会污染您的应用程序逻辑和应用程序状态 + 可测试性比这更好。您不依赖 thunk,并且您的逻辑并非在每个 redux 周期上都运行。
  • 我最初的帖子中的假设是,根据我的示例调度,您希望“远程传递道具”到组件中。在这种情况下,props 被放入 Redux 存储中,“对话管理器”组件从存储中读取它们并呈现实际的模态。因此,您不能合法地将函数作为道具传递给对话框,因为 Redux 存储中不允许使用函数。所以,我们必须以某种方式解决这个限制。
  • @markerikson 这是 IMO 反模式,您只需将其与您的商店耦合就破坏了可重用性。在这种情况下,从可以并且应该连接到商店的父组件传递道具有什么问题?在我看来,这个答案(即使它直接回答问题)具有误导性。
  • 我提供的答案对于所提出的问题完全有效。在 React (+Redux) 应用程序中有多种处理模式的方法。我在几个应用程序中使用的方法是通过单个根 &lt;ModalParent&gt; 组件驱动所有模态显示,并让应用程序的任何部分通过调度操作触发显示模态。您是否喜欢这种方法取决于您自己,但这是一种有效的潜在方法。
【解决方案2】:

感谢markerikson 提供答案。这启发了我创建一个带有 thunk 的解决方案。请在这里给我一些反馈:) 我将在我的示例中使用 hooks 和 @reduxjs/toolkit。

这是我的ConfirmationModalreducer 的状态:

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

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

这是切片(reducer 和动作的组合):

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;

这是它的 thunk 动作:

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;

您会注意到它使用extra.store 来执行subscribe。我们需要在创建商店时提供:

import combinedReducers 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: combinedReducers,
    middleware: customizedMiddleware,
  });
  
  thunkExtraArguments.store = store;

  return store;
}

export default createStore();

现在,让我们创建一个钩子,允许我们调度上述所有操作:

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,
  };
}

(不要忘记将confirmcancel 附加到您的模态按钮中)

就是这样!我们现在可以发送我们的确认模式:

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

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

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

  return {
    handleDelete,
  };
}

【讨论】:

  • 看起来可行。我看到的最重要的事情是你永远不会从那里的商店退订,你真的需要确保你这样做。此外,将商店本身作为“额外参数”传递进来有点粗略:)很惊讶它的工作原理,tbh。就我个人而言,我会继续为这种用例编写一个自定义中间件。
  • 对不起,这伤害了我的眼睛。对于像带有反应的通用模态这样简单的事情,您需要另外两个库,您正在使用 UI 逻辑污染应用程序状态(如果您的应用程序在不同平台上使用不同的模态怎么办?),并且为此编写单元测试必须是绝对的背痛,因为你像政治家一样到处许诺。
  • @webduvet 您建议使用 Redux 的其他什么解决方案来创建通用确认模式?我为提议的解决方案编写了单元测试,它很简单。
  • 我认为它应该是一个可重用的组件,因此它应该完全独立于您的应用程序设置。我会假设一个简单的 API,您可以在其中传递只是简单回调的处理程序。查看 material-UI 是如何做到的。
猜你喜欢
  • 2017-02-04
  • 2020-07-12
  • 2018-03-19
  • 1970-01-01
  • 2018-06-03
  • 2016-10-31
  • 2016-06-12
  • 1970-01-01
  • 2019-03-23
相关资源
最近更新 更多