如何在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)
我们在这里做了什么? ModalRoot
从state.modal
连接的ModalRoot
读取当前的modalType
和modalProps
,并渲染相应的组件,如DeletePostModal
或ConfirmLogoutModal
。 每个模态都是一个组件!
编写特定的模态组件
这里没有一般规则。 它们只是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-modal
( react-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层次结构不匹配。
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>) // ...