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

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

比方说,我想删除的东西,然后我会派遣一个像deleteSomething(id)的行动,所以一些减速器会捕获该事件,并将填充对话框减速器,以显示它。

当这个对话框提交时,我怀疑。

  • 该组件如何根据第一个动作派发适当的动作?
  • 行动创造者应该处理这个逻辑吗?
  • 我们可以在减速器内添加动作吗?

编辑:

使其更清晰:

 deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id) createThingB(id) => Show dialog with Questions => createThingBRemotely(id) 

所以我试图重用对话框组件。 显示/隐藏对话框不是问题,因为这可以很容易地在减速器中完成。 我想指定的是如何根据在左侧开始stream动的动作从右侧分配动作。

我build议的方法有点冗长,但我发现它可以很好地扩展到复杂的应用程序。 当你想显示一个模式,发起一个描述你想看到的模式的动作:

调度一个动作来显示模态

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

(当然,string可以是常量;为了简单,我使用内联string。)

编写一个Reducer来pipe理模态状态

然后确保你有一个接受这些值的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 */ }) 

大! 现在,当你发送一个动作时, state.modal将会更新以包含关于当前可见模态窗口的信息。

编写根模态组件

在组件层次结构的根目录中,添加连接到Redux存储的<ModalRoot>组件。 它将监听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) 

我们在这里做了什么? ModalRootstate.modal连接的ModalRoot读取当前的modalTypemodalProps ,并渲染相应的组件,如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) 

DeletePostModal连接到商店,因此它可以显示post标题,并像任何连接的组件一样工作:它可以在需要隐藏自己时调度操作,包括hideModal

提取演示组件

复制粘贴每个“特定”模式的相同布局逻辑将会很尴尬。 但是你有组件,对吧? 所以你可以提取一个不知道特定的模态是什么的expression式的<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时,我们通常要隐藏它们。

我build议你不要自己去实现它,而应该给你build议。 考虑无障碍是很难得到正确的。

相反,我build议你使用可访问的现成模态组件,如react-modalreact-modal 。 它是完全可定制的,你可以把任何你想要的内容,但它正确处理可访问性,使盲人仍然可以使用你的模态。

你甚至可以在你自己的<Modal>中包装react-modal ,它接受特定于你的应用程序的道具并生成子button或其他内容。 这只是组件!

其他方法

有不止一种方法来做到这一点。

有些人不喜欢这种方法的冗长性,并且倾向于使用一种名为“门户”的技术来使用<Modal>组件来在其组件内渲染它们 。 门户让你在你的内部渲染一个组件,而实际上它将在DOM中的预定位置呈现,这对于模态非常方便。

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

我鼓励你考虑两种方法,尝试一下,并select你最适合你的应用和你的团队。

使用门户

丹·阿布拉莫夫回答第一部分是好的,但涉及到很多样板。 正如他所说,你也可以使用门户。 我会扩大一点这个想法。

门户的优点是popup窗口和button与React树非常接近,通过使用道具进行非常简单的父/子通信:您可以轻松处理带有门户的asynchronous操作,或让父母自定义门户。

什么是门户网站?

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

这个想法是,例如,你正在渲染下面的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组件上的元素:popup窗口,下拉菜单,build议,热点

为什么要使用门户

不再有z-index问题 :门户允许你呈现给<body> 。 如果你想显示一个popup或下拉菜单,这是一个非常好的主意,如果你不想与Z指数问题作斗争。 门户网站的元素会按照挂载顺序添加document.body ,这意味着除非你使用z-index ,否则默认的行为是按照挂载顺序堆叠门户。 实际上,这意味着您可以安全地从另一个popup窗口中打开一个popup窗口,并确保第二个popup窗口将显示在第一个窗口的顶部,而不必考虑z-index

在实践中

最简单的方法是:使用本地React状态:如果你认为,对于一个简单的删除确认popup窗口来说,不需要Redux样板,那么你可以使用一个门户网站,它大大简化了你的代码。 对于这样的用户来说,交互非常本地化,实际上是一个实现细节,你真的关心热重载,时间旅行,动作logging和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状态 :如果你真的想,你仍然可以使用connect来select是否显示DeleteConfirmationPopup 。 由于门户网站仍深深地嵌套在您的React树中,所以定制此门户网站的行为非常简单,因为您的父母可以将道具传递给门户网站。 如果您不使用门户网站,您通常必须将您的popup窗口渲染到您的React树的顶部,以获得z-index原因,并且通常不得不考虑像“我如何根据用途自定义通用DeleteConfirmationPopup案件”。 通常你会发现这个问题很棘手的解决scheme,比如调度一个包含嵌套的确认/取消操作的动作,一个翻译绑定键,甚至更糟的是一个渲染函数(或其他不可序列化的东西)。 您不必使用门户网站,只需传递常规道具,因为DeleteConfirmationPopup只是DeleteButton

结论

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

请注意,门户实现也可以帮助您使用其他有用的function,如:

  • 无障碍
  • Espaceclosures门户的捷径
  • 处理外部点击(closures门户或不)
  • 处理链接点击(closures门户或不)
  • React上下文在门户树中可用

react-portal或react-modal对于popup窗口,模式和叠加层来说应该是全屏的,通常在屏幕中间居中。

react-tether是大多数React开发人员所不知道的,但它是您可以在其中find的最有用的工具之一。 Tether允许你创build门户,但是会自动定位相对于给定目标的门户。 这是工具提示,下拉菜单,热点,帮助框的完美select。如果您曾经对absolute / relative位置和z-index位置有任何问题,或者您的下拉列表在视口之外,Tether将为您解决所有这些问题。

例如,您可以轻松实现入门热点,一旦点击就展开到工具提示:

入职热点

这里真正的生产代码。 不能更简单:)

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

编辑 :刚刚发现react-gateway允许门户进入你select的节点(不一定是正文)

编辑 :它似乎反应波普尔可以是一个体面的替代反应,系绳。 PopperJS是一个只为某个元素计算适当位置的库,不需要直接触摸DOM,让用户select何时何地放置DOM节点,而Tether直接附加到body。

编辑 :下一个版本的React(光纤:大概16或17)将包括一个方法来创build门户: ReactDOM.unstable_createPortal() 链接

JS社区的知名专家可以在这里find很多好的解决scheme和有价值的评论。 这可能是一个指标,它可能看起来并不是那么简单的问题。 我想这就是为什么它可能成为这个问题的疑虑和不确定性的根源。

这里的基本问题是,在React中,只允许将组件加载到其父组件,而这并不总是所需的行为。 但是如何解决这个问题呢?

我提出解决scheme,解决这个问题。 更详细的问题定义,src和例子可以在这里find: https : //github.com/fckt/react-layer-stack#rationale

合理

react / react-dom来与2个基本的假设/想法:

  • 每个UI自然是分层的。 这就是为什么我们有构思相互包装的想法
  • react-dom默认情况下(实际上)将子组件挂载到其父DOM节点

问题是,有时第二个属性不是你想要在你的情况下。 有时候你想要把你的组件挂载到不同的物理DOM节点上,并同时保持父子之间的逻辑连接。

规范的例子是类似Tooltip的组件:在开发过程的某个时候,你可能会发现你需要为你的UI element添加一些描述:它会渲染到固定层,并且应该知道它的坐标(这是UI element坐标或鼠标coords),同时它需要信息是否需要现在或不需要显示,其内容和来自父组件的一些背景。 这个例子表明有时逻辑层次结构与物理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>) // ...