如何在 Redux 中显示执行异步操作的模式对话框?使用门户何时使用门户为什么要使用门户在实践中

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

假设我想删除某些内容,然后我会调度一个操作,以便某个化简器将捕获该事件并填充对话框化简器以显示它。deleteSomething(id)

当这个对话框提交时,我的怀疑就来了。

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

编辑:

以使其更清晰:

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

createThingB(id) => Show dialog with Questions => createThingBRemotely(id)

因此,我正在尝试重用对话框组件。显示/隐藏对话框不是问题,因为这可以在减速器中轻松完成。我试图指定的是如何根据在左侧启动流的操作从右侧调度操作。


答案 1

我建议的方法有点冗长,但我发现它可以很好地扩展到复杂的应用程序中。当您想要显示模式时,请触发一个描述您希望看到模式的操作:

调度操作以显示模式

this.props.dispatch({
  type: 'SHOW_MODAL',
  modalType: 'DELETE_POST',
  modalProps: {
    postId: 42
  }
})

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

编写化简器来管理模态

然后,请确保您有一个只接受这些值的化简器:

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 */
})

伟大!现在,当您调度操作时,将更新以包含有关当前可见模式窗口的信息。state.modal

编写根模态组件

在组件层次结构的根目录中,添加连接到 Redux 存储的组件。它将侦听并显示适当的模式组件,从 .<ModalRoot>state.modalstate.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)

我们在这里做了什么? 读取电流和它所连接的电流,并呈现相应的组件,如 或 。每个模态都是一个组件!ModalRootmodalTypemodalPropsstate.modalDeletePostModalConfirmLogoutModal

编写特定模态组件

这里没有一般规则。它们只是 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)

连接到商店,因此它可以显示帖子标题,并且像任何连接的组件一样工作:它可以调度操作,包括何时需要隐藏自身。DeletePostModalhideModal

提取表示组件

为每个“特定”模式复制粘贴相同的布局逻辑会很尴尬。但是你有组件,对吧?因此,您可以提取一个表示组件,该组件不知道特定模式的作用,但处理它们的外观。<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)

由您来提出一组可以在您的应用程序中接受的道具,但我想您可能有几种模态(例如信息模态,确认模态等),以及它们的几种样式。<Modal>

点击外部或转义键时的可访问性和隐藏

关于模式的最后一个重要部分是,通常我们希望在用户单击外部或按Escape时隐藏它们。

我建议你不要给你关于实现它的建议,而是建议你不要自己实现它。考虑到可访问性,很难正确。

相反,我建议您使用可访问的现成模态组件,例如 react-modal。它是完全可定制的,你可以把任何你想要的东西放在里面,但它正确地处理了可访问性,所以盲人仍然可以使用你的模态。

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

其他方法

有多种方法可以做到这一点。

有些人不喜欢这种方法的冗长性,他们更喜欢有一个组件,他们可以使用一种称为“门户”的技术在其组件中呈现。门户允许您在组件内部渲染组件,而实际上它将在DOM中的预定位置进行渲染,这对于模式来说非常方便。<Modal>

事实上,我之前链接到的反应模态已经在内部做到了这一点,所以从技术上讲,你甚至不需要从顶部渲染它。我仍然发现将我想显示的模态与显示它的组件分离是很好的,但是您也可以直接从组件中使用,并跳过我上面写的大部分内容。react-modal

我鼓励你考虑这两种方法,尝试它们,并选择你认为最适合你的应用和团队的方法。


答案 2

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

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


使用门户

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

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

什么是门户?

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

这个想法是,例如,您将以下 React 树渲染到正文中:

<div className="layout">
  <div className="outside-portal">
    <Portal>
      <div className="inside-portal">
        PortalContent
      </div>
    </Portal>
  </div>
</div>

你得到的输出是:

<body>
  <div class="layout">
    <div class="outside-portal">
    </div>
  </div>
  <div class="inside-portal">
    PortalContent
  </div>
</body>

节点已在 内部平移,而不是其正常的深度嵌套位置。inside-portal<body>

何时使用门户

门户对于显示应位于现有 React 组件之上的元素特别有用:弹出窗口、下拉列表、建议、热点

为什么要使用门户

不再有 z 索引问题:门户允许您渲染到 。如果你想显示一个弹出窗口或下拉列表,如果你不想与z索引问题作斗争,这是一个非常好的主意。门户元素按装载顺序添加,这意味着除非您使用 ,否则默认行为是按装载顺序将门户堆叠在彼此之上。在实践中,这意味着您可以从另一个弹出窗口内部安全地打开弹出窗口,并确保第二个弹出窗口将显示在第一个弹出窗口的顶部,甚至不必考虑 。<body>document.bodyz-indexz-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>
    );
  }
}

很简单:你仍然可以使用Redux状态:如果你真的想要,你仍然可以使用来选择是否显示。由于门户仍然深深地嵌套在 React 树中,因此自定义此门户的行为非常简单,因为您的父级可以将 props 传递给门户。如果你不使用门户,你通常不得不在 React 树的顶部渲染你的弹出窗口,并且通常必须考虑诸如“我如何自定义我根据用例构建的通用 DeleteConfirmationPopup”这样的事情。通常,您会发现这个问题的解决方案非常棘手,例如调度包含嵌套确认/取消操作,翻译捆绑键的操作,甚至更糟的是,渲染函数(或其他不可序列化的功能)。你不必用传送门来做,只需要传递常规道具,因为它只是一个孩子connectDeleteConfirmationPopupz-indexDeleteConfirmationPopupDeleteButton

结论

门户对于简化代码非常有用。我再也离不开他们了。

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

  • 可及性
  • 用于关闭门户的空格键快捷方式
  • 处理外部单击(是否关闭门户)
  • 处理链接单击(是否关闭门户)
  • 在门户树中可用的 React 上下文

react-portalreact-modal 非常适合弹出窗口、模式和叠加,它们应该是全屏的,通常位于屏幕中间。

react-tether 对于大多数 React 开发人员来说是未知的,但它是你可以找到的最有用的工具之一。Tether 允许您创建门户,但会相对于给定目标自动定位门户。这非常适合工具提示,下拉列表,热点,帮助框...如果您曾经遇到过任何位置/和的问题,或者您的下拉列表超出您的视口,Tether将为您解决所有这些问题。absoluterelativez-index

例如,您可以轻松实现载入热点,单击后该热点将扩展为工具提示:

Onboarding hotspot

真正的生产代码在这里。再简单不过:)

<MenuHotspots.contacts>
  <ContactButton/>
</MenuHotspots.contacts>

编辑:刚刚发现的反应网关,它允许将门户渲染到您选择的节点(不一定是正文)

编辑:似乎react-popper可以成为react-tether的一个不错的替代品。PopperJS是一个库,它只计算元素的适当位置,而不直接接触DOM,让用户选择他想要放置DOM节点的位置和时间,而Tether则直接附加到主体。

编辑:还有react-slot-fill,这很有趣,可以通过允许将元素渲染到保留的元素插槽来帮助解决类似的问题,您可以将该元素放在树中所需的任何位置