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

假设我想删除一些东西,然后分派一个动作比如deletessomething (id)这样一些reducer就会捕捉到那个事件并填充对话减速器来显示它。

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

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

编辑:

更清楚地说:

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

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

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


我建议的方法有点啰嗦,但我发现它可以很好地扩展到复杂的应用程序。当你想要显示一个模态时,触发一个动作来描述你想要看到的模态:

调度一个动作来显示模态

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

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

编写一个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 */
})

太棒了!现在,当您分派一个动作时,请声明。模态将更新到包含当前可见的模态窗口的信息。

编写根模态组件

在组件层次结构的根,添加一个连接到Redux存储的<ModalRoot>组件。它将听取国家的意见。并显示适当的模态组件,从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从状态中读取当前的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连接到存储,因此它可以显示文章标题,并像任何连接的组件一样工作:它可以分派动作,包括在需要隐藏自身时分派hideModal。

提取一个表示组件

为每个“特定的”模态复制粘贴相同的布局逻辑会很尴尬。但是你有组件,对吧?因此,您可以提取一个presentational <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。它是完全可定制的,你可以把任何你想要的东西放在里面,但它正确地处理可访问性,以便盲人仍然可以使用你的模式。

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

其他方法

做这件事的方法不止一种。

有些人不喜欢这种方法的冗长,他们更喜欢有一个<Modal>组件,他们可以使用一种称为“门户”的技术在组件中呈现该组件。门户可以让你在你的组件中呈现一个组件,而实际上它会在DOM中预先确定的位置呈现,这对于模态非常方便。

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

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


更新:React 16.0通过React dom引入了门户。createPortal链接

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


使用门户网站

丹·阿布拉莫夫回答的第一部分很好,但涉及很多样板。正如他所说,您还可以使用传送门。我将在这个观点上展开一点。

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

什么是门户?

门户允许您直接在文档中呈现。body是一个嵌套在React树中的元素。

例如,你将下面的React树渲染到body中:

<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>

内部门户节点已转换到<body>内,而不是正常的深层嵌套位置。

何时使用门户

门户对于显示应该放在现有React组件之上的元素特别有帮助:弹出窗口、下拉菜单、建议、热点

为什么使用门户

No z-index problems anymore: a portal permits you to render to <body>. If you want to display a popup or dropdown, this is a really nice idea if you don't want to have to fight against z-index problems. The portal elements get added do document.body in mount order, which means that unless you play with z-index, the default behavior will be to stack portals on top of each others, in mounting order. In practice, it means that you can safely open a popup from inside another popup, and be sure that the 2nd popup will be displayed on top of the first, without having to even think about 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>
    );
  }
}

Simple: you can still use Redux state: if you really want to, you can still use connect to choose whether or not the DeleteConfirmationPopup is shown or not. As the portal remains deeply nested in your React tree, it is very simple to customize the behavior of this portal because your parent can pass props to the portal. If you don't use portals, you usually have to render your popups at the top of your React tree for z-index reasons, and usually have to think about things like "how do I customize the generic DeleteConfirmationPopup I built according to the use case". And usually you'll find quite hacky solutions to this problem, like dispatching an action that contains nested confirm/cancel actions, a translation bundle key, or even worse, a render function (or something else unserializable). You don't have to do that with portals, and can just pass regular props, since DeleteConfirmationPopup is just a child of the DeleteButton

结论

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

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

可访问性 Espace可以通过快捷方式关闭门户 处理外部单击(是否关闭门户) 处理链接单击(是否关闭门户) React上下文在门户树中可用

React-portal或react-modal很适合弹出窗口、模式和覆盖,这些应该是全屏的,通常在屏幕中间居中。

大多数React开发者都不知道React -tether,但它是你能找到的最有用的工具之一。Tether允许您创建门户,但将相对于给定的目标自动定位门户。这是完美的工具提示,下拉菜单,热点,帮助框…如果你曾经有任何问题的位置绝对/相对和z指数,或你的下拉到你的视口之外,Tether将为你解决这一切。

例如,你可以轻松地实现入职热点,点击后扩展为工具提示:

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

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

编辑:刚刚发现了react-gateway,它允许将门户呈现到您选择的节点(不一定是body)

编辑:似乎反应-popper可以是一个不错的替代反应-tether。PopperJS是一个库,它只计算元素的适当位置,而不直接接触DOM,让用户选择何时将DOM节点放在哪里,而Tether则直接追加到主体。

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


在这里可以找到许多来自JS社区的知名专家关于这个主题的很好的解决方案和有价值的评论。这可能是一个指标,表明这不是一个看似微不足道的问题。我认为这就是为什么它可能成为对该问题的怀疑和不确定性的来源。

这里最根本的问题是在React中,你只允许将组件挂载到它的父组件上,这并不总是理想的行为。但是如何解决这个问题呢?

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

Rationale react/react-dom comes comes with 2 basic assumptions/ideas: every UI is hierarchical naturally. This why we have the idea of components which wrap each other react-dom mounts (physically) child component to its parent DOM node by default The problem is that sometimes the second property isn't what you want in your case. Sometimes you want to mount your component into different physical DOM node and hold logical connection between parent and child at the same time. Canonical example is Tooltip-like component: at some point of development process you could find that you need to add some description for your UI element: it'll render in fixed layer and should know its coordinates (which are that UI element coord or mouse coords) and at the same time it needs information whether it needs to be shown right now or not, its content and some context from parent components. This example shows that sometimes logical hierarchy isn't match with the physical DOM hierarchy.

看看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>)
// ...

在我看来,最基本的实现有两个要求。跟踪模式是否打开的状态,以及在标准react树之外呈现模式的门户。

下面的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;

这里有一个简单的用例……

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;

我使用呈现函数,因为我想将状态管理和样板逻辑与呈现的模态和触发器组件的实现隔离开来。这允许呈现的组件成为你想要的任何东西。在您的例子中,我认为模态组件可以是一个连接的组件,它接收一个分派异步操作的回调函数。

如果你需要从触发器组件向模态组件发送动态道具(希望这种情况不会经常发生),我建议用容器组件包装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;

将模态包装到连接的容器中,并在这里执行异步操作。通过这种方式,您可以同时到达触发动作的分派和onClose道具。从道具到达调度,不通过mapDispatchToProps函数连接。

class ModalContainer extends React.Component {
  handleDelete = () => {
    const { dispatch, onClose } = this.props;
    dispatch({type: 'DELETE_POST'});

    someAsyncOperation().then(() => {
      dispatch({type: 'DELETE_POST_SUCCESS'});
      onClose();
    })
  }

  render() {
    const { onClose } = this.props;
    return <Modal onClose={onClose} onSubmit={this.handleDelete} />
  }
}

export default connect(/* no map dispatch to props here! */)(ModalContainer);

模式被渲染并设置其可见性状态的App:

class App extends React.Component {
  state = {
    isModalOpen: false
  }

  handleModalClose = () => this.setState({ isModalOpen: false });

  ...

  render(){
    return (
      ...
      <ModalContainer onClose={this.handleModalClose} />  
      ...
    )
  }

}