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

Javascript 如何在Redux中显示执行异步操作的模式对话框?,javascript,modal-dialog,redux,react-redux,Javascript,Modal Dialog,Redux,React Redux,我正在构建一个在某些情况下需要显示确认对话框的应用程序 假设我想删除某个内容,然后我将发送一个类似于deleteSomething(id)的操作,这样一些reducer将捕获该事件并填充对话框reducer以显示它 当这个对话提交时,我产生了怀疑 该组件如何根据已调度的第一个操作调度正确的操作 动作创建者应该处理这个逻辑吗 我们可以在减速器内添加动作吗 编辑: 更清楚地说: deleteThingA(id) => show dialog with Questions => del

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

假设我想删除某个内容,然后我将发送一个类似于
deleteSomething(id)
的操作,这样一些reducer将捕获该事件并填充对话框reducer以显示它

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

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

更清楚地说:

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

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

所以我尝试重用对话框组件。显示/隐藏对话框这不是问题,因为这可以在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 */
})
太好了!现在,当您分派一个操作时,
state.modal
将更新以包含有关当前可见的模态窗口的信息

编写根模态组件 在组件层次结构的根目录下,添加连接到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)
DeletePostModal
连接到存储,因此它可以显示帖子标题,并像任何连接的组件一样工作:当需要隐藏自己时,它可以调度操作,包括
hideModal

提取表示成分 对于每个“特定”模式,复制粘贴相同的布局逻辑会很尴尬。但是你有组件,对吗?因此,您可以提取一个组件,该组件不知道特定模态的作用,但可以处理它们的外观

然后,特定的模态,如
deletePostModel
可以使用它进行渲染:

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)
从“../actions”导入{deletePost,hideModal}
从“./Modal”导入模态
constdeletepostmodal=({post,dispatch})=>(
分派(deletePost(post.id))。然后(()=>{
分派(hideModal())
})
})
/>
)
导出默认连接(
(状态,ownProps)=>({
post:state.postsById[ownProps.postId]
})
)(邮递)
这取决于您是否能在应用程序中提供一套
可以接受的道具,但我认为您可能有几种模态(例如,信息模态、确认模态等),以及它们的几种样式

可访问性和隐藏单击外部或转义键 关于情态动词的最后一个重要部分是,通常我们希望在用户单击外部或按Escape时隐藏它们

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

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

您甚至可以在自己的
中包装
react model
,它接受特定于应用程序的道具并生成子按钮或其他内容。这些都是组件

其他办法 做这件事的方法不止一种

有些人不喜欢这种方法的冗长,他们更喜欢使用一种称为“门户”的技术,在组件内部设置一个
组件。门户允许您渲染内部组件,而实际上它将在DOM中的预定位置进行渲染,这对于modals非常方便

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


<>我鼓励你考虑这两种方法,试验它们,选择你认为最适合你的应用程序和你的团队的工作。

< P> <强> Update <强>:通过“代码> ReaTrDOM”创建16个门户网站。 更新:React的下一个版本(光纤:可能16或17)将包括一种创建门户的方法:
ReactDOM.unstable\u createPortal()


使用门户 丹·阿布拉莫夫的回答第一部分很好,但涉及很多样板。正如他所说,您也可以使用门户。我将进一步阐述这个想法

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

什么是门户? 门户允许您直接在
document.body
中呈现深入嵌套在React树中的元素

例如,您可以将以下反应树渲染到主体中:

<div className="layout">
  <div className="outside-portal">
    <Portal>
      <div className="inside-portal">
        PortalContent
      </div>
    </Portal>
  </div>
</div>
简单:您仍然可以使用Redux state:如果确实需要,您仍然可以使用
connect
来选择是否显示
DeleteConfirmationPopup
。由于门户仍然深深嵌套在您的React树中,因此定制
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)
<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>
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>
    );
  }
}
<MenuHotspots.contacts>
  <ContactButton/>
</MenuHotspots.contacts>
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>)
// ...
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;
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;
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;