如何在Redux中显示执行异步操作的模式对话框?

car*_*sba 229 javascript modal-dialog redux react-redux

我正在构建一个需要在某些情况下显示确认对话框的应用程序.

假设我想删除一些东西,然后我会调度一个动作,deleteSomething(id)因此一些reducer会捕获该事件并填充对话框减速器以显示它.

当这个对话提交时,我怀疑了.

  • 该组件如何根据调度的第一个操作调度正确的操作?
  • 动作创建者应该处理这个逻辑吗?
  • 我们可以在减速机内添加动作吗?

编辑:

使它更清楚:

deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id)

createThingB(id) => Show dialog with Questions => createThingBRemotely(id)
Run Code Online (Sandbox Code Playgroud)

所以我正在尝试重用对话框组件.显示/隐藏对话框不是问题,因为这可以在reducer中轻松完成.我想要指定的是如何根据左侧开始流动的动作从右侧调度动作.

Dan*_*mov 491

我建议的方法有点冗长但我发现它可以很好地扩展到复杂的应用程序.当您想要显示模态时,触发描述您想要查看哪个模态的动作:

调度操作以显示模态

this.props.dispatch({
  type: 'SHOW_MODAL',
  modalType: 'DELETE_POST',
  modalProps: {
    postId: 42
  }
})
Run Code Online (Sandbox Code Playgroud)

(字符串当然可以是常量;为简单起见,我使用内联字符串.)

编写Reducer来管理模态

然后确保你有一个只接受这些值的reducer:

const initialState = {
  modalType: null,
  modalProps: {}
}

function modal(state = initialState, action) {
  switch (action.type) {
    case 'SHOW_MODAL':
      return {
        modalType: action.modalType,
        modalProps: action.modalProps
      }
    case 'HIDE_MODAL':
      return initialState
    default:
      return state
  }
}

/* .... */

const rootReducer = combineReducers({
  modal,
  /* other reducers */
})
Run Code Online (Sandbox Code Playgroud)

大!现在,当您分派操作时,state.modal将更新以包含有关当前可见模态窗口的信息.

编写根模式组件

在组件层次结构的根目录中,添加<ModalRoot>连接到Redux存储的组件.它会监听state.modal并显示一个合适的模态组件,从中转发道具state.modal.modalProps.

// These are regular React components we will write soon
import DeletePostModal from './DeletePostModal'
import ConfirmLogoutModal from './ConfirmLogoutModal'

const MODAL_COMPONENTS = {
  'DELETE_POST': DeletePostModal,
  'CONFIRM_LOGOUT': ConfirmLogoutModal,
  /* other modals */
}

const ModalRoot = ({ modalType, modalProps }) => {
  if (!modalType) {
    return <span /> // after React v15 you can return null here
  }

  const SpecificModal = MODAL_COMPONENTS[modalType]
  return <SpecificModal {...modalProps} />
}

export default connect(
  state => state.modal
)(ModalRoot)
Run Code Online (Sandbox Code Playgroud)

我们在这做了什么?ModalRoot读取当前modalTypemodalPropsstate.modal其所连接的,并呈现对应的组件,例如DeletePostModalConfirmLogoutModal.每个模态都是一个组件!

编写特定的模态组件

这里没有一般规则.它们只是React组件,可以调度操作,从存储状态读取内容,恰好是模态.

例如,DeletePostModal可能看起来像:

import { deletePost, hideModal } from '../actions'

const DeletePostModal = ({ post, dispatch }) => (
  <div>
    <p>Delete post {post.name}?</p>
    <button onClick={() => {
      dispatch(deletePost(post.id)).then(() => {
        dispatch(hideModal())
      })
    }}>
      Yes
    </button>
    <button onClick={() => dispatch(hideModal())}>
      Nope
    </button>
  </div>
)

export default connect(
  (state, ownProps) => ({
    post: state.postsById[ownProps.postId]
  })
)(DeletePostModal)
Run Code Online (Sandbox Code Playgroud)

DeletePostModal连接到存储,以便它可以显示文章标题和像任何连接的组件:可以调度的行动,包括hideModal在必要时可以隐藏自身.

提取演示组件

为每个"特定"模态复制粘贴相同的布局逻辑会很尴尬.但你有组件,对吗?因此,您可以提取一个表示 <Modal>组件,该组件不知道特定模态的作用,但会处理它们的外观.

然后,特定的模态,如DeletePostModal可以用于渲染:

import { deletePost, hideModal } from '../actions'
import Modal from './Modal'

const DeletePostModal = ({ post, dispatch }) => (
  <Modal
    dangerText={`Delete post ${post.name}?`}
    onDangerClick={() =>
      dispatch(deletePost(post.id)).then(() => {
        dispatch(hideModal())
      })
    })
  />
)

export default connect(
  (state, ownProps) => ({
    post: state.postsById[ownProps.postId]
  })
)(DeletePostModal)
Run Code Online (Sandbox Code Playgroud)

<Modal>可以在你的应用程序中提出一套可以接受的道具,但我想你可能有几种模态(例如信息模态,确认模态等),以及它们的几种风格.

可访问性和隐藏单击外部或退出键

关于模态的最后一个重要部分是,当用户点击外部或按Escape时,我们通常希望隐藏它们.

我建议你不要自己实现它,而不是给你实现这个的建议.考虑可访问性很难做到正确.

相反,我建议你使用一个可访问的现成模态组件,如react-modal.它是完全可定制的,您可以在其中放置任何您想要的内容,但它可以正确处理可访问性,以便盲人仍然可以使用您的模态.

您甚至react-modal可以自己包装<Modal>,接受特定于您的应用程序的道具,并生成子按钮或其他内容.这一切都只是组件!

其他方法

有不止一种方法可以做到这一点.

有些人不喜欢这种方法的冗长,并且更喜欢使用一种称为"门户"的技术<Modal>,使用可以在组件内部呈现的组件.Portals允许您在您的内部渲染组件,而实际上它将在DOM中的预定位置渲染,这对于模态非常方便.

事实上,react-modal我之前已经链接到内部,所以在技术上你甚至不需要从顶部渲染它.我仍然觉得将我想要显示的模态与显示它的组件分离很好,但你也可以react-modal直接从你的组件中使用,并跳过我上面写的大部分内容.

我鼓励您考虑这两种方法,尝试使用它们,然后选择最适合您的应用和团队的方法.

  • 我建议的一件事是让reducer维护一个可以推送和弹出的模态列表.听起来很傻,我一直遇到设计师/产品类型要求我从模态中打开模态的情况,并且很高兴允许用户"回去". (34认同)
  • 是的,当然,这是Redux易于构建的东西,因为你可以将状态改为数组.就个人而言,我曾与设计师合作,相反,他们希望模态是独占的,所以我写的方法解决了意外嵌套.但是,是的,你可以两种方式. (8认同)
  • 有时我想在模态关闭后调用某些函数(例如,使用模态中的输入字段值调用函数).我将这些函数作为`modalProps`传递给动作.这违反了保持状态可序列化的规则.我怎样才能克服这个问题? (6认同)
  • 根据我的经验,我会说:如果模态与本地组件相关(如删除确认模式与删除按钮相关),则使用门户网站更简单,否则使用redux操作.同意@Kyle应该能够从模态中打开模态.默认情况下它也适用于门户网站,因为它们是为了记录正文而添加的,因此门户网站很好地堆叠在一起(直到你把所有东西都搞乱了z-index:p) (4认同)
  • @DanAbramov,你的解决方案很棒,但我有一个小问题.不严重.我在项目中使用Material-ui,当关闭模态时它只是将其关闭,而不是"播放"淡出动画.可能需要做一些延迟?或者将每个模态作为ModalRoot内的列表保存?建议? (4认同)
  • 有没有办法从父容器/组件传递一个函数,一旦模态关闭就可以调用它?我可以在按下按钮时从模态调用动作,但是当模态关闭时我想要一个回调,这将允许我在父按钮上执行一个函数.父加载 - >按钮单击调度模式 - >确认模态调度操作然后调度hideModal - >调用容器中的父函数,如果确认成功则页面被重定向. (3认同)
  • 更新:看起来函数可以在props中传递.所以现在我的模态确认将假检查传入的函数,然后在保存成功后调用它. (2认同)
  • 对不起,有人可以在上面的deletePostModal代码片段中解释`dispatch`的来源吗?当我们只传递`post`属性时,为什么它应该在props对象中呢? (2认同)

Seb*_*ber 92

更新:React 16.0通过链接引入了门户ReactDOM.createPortal

更新:React的下一个版本(光纤:可能是16或17)将包含一个创建门户的方法:ReactDOM.unstable_createPortal() 链接


使用门户网站

Dan Abramov回答第一部分很好,但涉及很多样板.正如他所说,你也可以使用门户网站.我会对这个想法进行一些扩展.

门户网站的优点是弹出窗口和按钮保持非常靠近React树,使用props进行非常简单的父/子通信:您可以轻松处理与门户网站的异步操作,或让父级自定义门户网站.

什么是门户网站?

门户允许您直接在document.body深层嵌套在React树中的元素内部进行渲染.

例如,您可以将以下React树渲染到正文中:

<div className="layout">
  <div className="outside-portal">
    <Portal>
      <div className="inside-portal">
        PortalContent
      </div>
    </Portal>
  </div>
</div>
Run Code Online (Sandbox Code Playgroud)

你得到输出:

<body>
  <div class="layout">
    <div class="outside-portal">
    </div>
  </div>
  <div class="inside-portal">
    PortalContent
  </div>
</body>
Run Code Online (Sandbox Code Playgroud)

inside-portal节点已在内部翻译<body>,而不是其正常的深层嵌套位置.

何时使用门户网站

门户网站特别有助于显示应该在现有React组件之上的元素:弹出窗口,下拉列表,建议,热点

为什么要使用门户网站

没有z-index问题:门户网站允许您渲染<body>.如果你想显示一个弹出窗口或下拉列表,如果你不想打击z-index问题,这是一个非常好的主意.门户元素document.body以挂载顺序添加,这意味着除非您使用z-index,否则默认行为是按安装顺序将门户堆叠在彼此之上.在实践中,这意味着您可以安全地从另一个弹出窗口中打开弹出窗口,并确保第二个弹出窗口将显示在第一个弹出窗口的顶部,而不必考虑z-index.

在实践中

最简单:使用本地React状态:如果您认为,对于简单的删除确认弹出窗口,不值得使用Redux样板,那么您可以使用门户网站,它可以大大简化您的代码.对于这样一个用例,交互是非常本地化的,实际上是一个实现细节,你真的关心热重载,时间旅行,动作日志记录以及Redux带给你的所有好处吗?就个人而言,在这种情况下我不会使用当地的州.代码变得如此简单:

class DeleteButton extends React.Component {
  static propTypes = {
    onDelete: PropTypes.func.isRequired,
  };

  state = { confirmationPopup: false };

  open = () => {
    this.setState({ confirmationPopup: true });
  };

  close = () => {
    this.setState({ confirmationPopup: false });
  };

  render() {
    return (
      <div className="delete-button">
        <div onClick={() => this.open()}>Delete</div>
        {this.state.confirmationPopup && (
          <Portal>
            <DeleteConfirmationPopup
              onCancel={() => this.close()}
              onConfirm={() => {
                this.close();
                this.props.onDelete();
              }}
            />
          </Portal>
        )}
      </div>
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

简单:您仍然可以使用Redux状态:如果您真的想要,您仍然可以使用它connect来选择是否DeleteConfirmationPopup显示.由于门户网站仍然深深嵌套在您的React树中,因此您可以非常简单地自定义此门户网站的行为,因为您的父级可以将道具传递给门户网站.如果你不使用门户,你通常必须在你的React树的顶部渲染你的弹出窗口,z-index原因是通常需要考虑"我如何自定义我根据用例构建的泛型DeleteConfirmationPopup".而且通常你会发现这个问题的解决方案相当苛刻,比如调度包含嵌套确认/取消操作的动作,翻译包密钥,甚至更糟糕的是渲染函数(或其他不可序列化的东西).您不必使用门户网站,也可以只传递常规道具,因为DeleteConfirmationPopup它只是一个孩子DeleteButton

结论

门户对于简化代码非常有用.我不能没有他们了.

请注意,门户实现还可以帮助您使用其他有用的功能,例如:

  • 无障碍
  • 用于关闭门户的Espace快捷方式
  • 处理外部点击(关闭门户网站与否)
  • 处理链接点击(关闭门户网站或不关闭)
  • React Context在门户树中可用

react-portalreact-modal适用于应该是全屏的弹出窗口,模态和叠加层,通常位于屏幕中间.

react-tether对于大多数React开发人员来说都是未知的,但它是你可以找到的最有用的工具之一.Tether允许您创建门户,但相对于给定目标,将自动定位门户.这是完美的工具提示,下拉菜单,热点helpboxes ......如果你曾经有过与位置有任何问题absolute/ relativez-index,或者你的下拉会视口之外,系绳将解决所有的为您服务.

例如,您可以轻松实现入职热点,一旦点击即可扩展为工具提示:

入职热点

真实的生产代码在这里 不能更简单:)

<MenuHotspots.contacts>
  <ContactButton/>
</MenuHotspots.contacts>
Run Code Online (Sandbox Code Playgroud)

编辑:刚刚发现react-gateway,允许将门户网站渲染到您选择的节点(不一定是正文)

编辑:似乎反应波普尔可以成为反应系统的一个不错的选择.PopperJS是一个只计算元素的适当位置的库,不直接触及DOM,让用户选择他想要放置DOM节点的位置和时间,而Tether直接附加到正文.

编辑:还有react-slot-fill这很有意思,它可以帮助解决类似的问题,允许将一个元素渲染到一个保留的元素槽,你可以在树中放置你想要的任何地方


fck*_*ckt 8

JS社区的知名专家就此主题提供了许多优秀的解决方案和有价值的评论.它可能是一个指标,它可能看起来不是那么微不足道的问题.我认为这就是为什么它可能成为问题的疑点和不确定性的原因.

这里的基本问题是,在React中,您只能将组件安装到其父组件,这并不总是所需的行为.但是如何解决这个问题呢?

我提出了解决这个问题的解决方案.更详细的问题定义,src和示例可以在这里找到:https://github.com/fckt/react-layer-stack#rationale

合理

react/ react-dom附带2个基本假设/想法:

  • 每个UI都是自然分层的.这就是为什么我们有components相互包装的想法
  • react-dom 默认情况下,将(子)物理子组件安装到其父DOM节点

问题是有时第二个属性不是你想要的.有时您希望将组件安装到不同的物理DOM节点中,同时保持父节点和子节点之间的逻辑连接.

典型的例子是类似工具提示的组件:在开发过程的某个阶段你可以发现你需要为你添加一些描述UI element:它将在固定层中渲染并且应该知道它的坐标(这是UI element坐标或鼠标坐标)和同时它需要信息是否需要立即显示,其内容和来自父组件的一些上下文.此示例显示有时逻辑层次结构与物理DOM层次结构不匹配.

请查看https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example以查看回答您问题的具体示例:

import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
  const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
  return (
    <Cell {...props}>
        // the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
        <Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
            hideMe, // alias for `hide(modalId)`
            index } // useful to know to set zIndex, for example
            , e) => // access to the arguments (click event data in this example)
          <Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
            <ConfirmationDialog
              title={ 'Delete' }
              message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
              confirmButton={ <Button type="primary">DELETE</Button> }
              onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
              close={ hideMe } />
          </Modal> }
        </Layer>

        // this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
        <LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
          <div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
            <Icon type="trash" />
          </div> }
        </LayerContext>
    </Cell>)
// ...
Run Code Online (Sandbox Code Playgroud)


ksk*_*ido 5

在我看来,最低限度的实施有两个要求。跟踪模态是否打开的状态,以及在标准反应树外部渲染模态的门户。

下面的 ModalContainer 组件实现了这些要求以及模态和触发器的相应渲染函数,触发器负责执行回调以打开模态。

import React from 'react';
import PropTypes from 'prop-types';
import Portal from 'react-portal';

class ModalContainer extends React.Component {
  state = {
    isOpen: false,
  };

  openModal = () => {
    this.setState(() => ({ isOpen: true }));
  }

  closeModal = () => {
    this.setState(() => ({ isOpen: false }));
  }

  renderModal() {
    return (
      this.props.renderModal({
        isOpen: this.state.isOpen,
        closeModal: this.closeModal,
      })
    );
  }

  renderTrigger() {
     return (
       this.props.renderTrigger({
         openModal: this.openModal
       })
     )
  }

  render() {
    return (
      <React.Fragment>
        <Portal>
          {this.renderModal()}
        </Portal>
        {this.renderTrigger()}
      </React.Fragment>
    );
  }
}

ModalContainer.propTypes = {
  renderModal: PropTypes.func.isRequired,
  renderTrigger: PropTypes.func.isRequired,
};

export default ModalContainer;
Run Code Online (Sandbox Code Playgroud)

这是一个简单的用例......

import React from 'react';
import Modal from 'react-modal';
import Fade from 'components/Animations/Fade';
import ModalContainer from 'components/ModalContainer';

const SimpleModal = ({ isOpen, closeModal }) => (
  <Fade visible={isOpen}> // example use case with animation components
    <Modal>
      <Button onClick={closeModal}>
        close modal
      </Button>
    </Modal>
  </Fade>
);

const SimpleModalButton = ({ openModal }) => (
  <button onClick={openModal}>
    open modal
  </button>
);

const SimpleButtonWithModal = () => (
   <ModalContainer
     renderModal={props => <SimpleModal {...props} />}
     renderTrigger={props => <SimpleModalButton {...props} />}
   />
);

export default SimpleButtonWithModal;
Run Code Online (Sandbox Code Playgroud)

我使用渲染函数,因为我想将状态管理和样板逻辑与渲染模式和触发器组件的实现隔离开来。这允许渲染的组件成为您想要的任何组件。在您的情况下,我认为模态组件可能是一个连接的组件,它接收调度异步操作的回调函数。

如果您需要从触发器组件将动态道具发送到模态组件(希望这种情况不会经常发生),我建议使用容器组件包装 ModalContainer,该组件在其自己的状态下管理动态道具并增强原始渲染方法,例如所以。

import React from 'react'
import partialRight from 'lodash/partialRight';
import ModalContainer from 'components/ModalContainer';

class ErrorModalContainer extends React.Component {
  state = { message: '' }

  onError = (message, callback) => {
    this.setState(
      () => ({ message }),
      () => callback && callback()
    );
  }

  renderModal = (props) => (
    this.props.renderModal({
       ...props,
       message: this.state.message,
    })
  )

  renderTrigger = (props) => (
    this.props.renderTrigger({
      openModal: partialRight(this.onError, props.openModal)
    })
  )

  render() {
    return (
      <ModalContainer
        renderModal={this.renderModal}
        renderTrigger={this.renderTrigger}
      />
    )
  }
}

ErrorModalContainer.propTypes = (
  ModalContainer.propTypes
);

export default ErrorModalContainer;
Run Code Online (Sandbox Code Playgroud)