web-dev-qa-db-fra.com

Comment puis-je afficher une boîte de dialogue modale dans Redux qui effectue des actions asynchrones?

Je construis une application qui doit afficher une boîte de dialogue de confirmation dans certaines situations.

Disons que je veux supprimer quelque chose, puis je déclencherai une action comme deleteSomething(id) afin qu'un réducteur attrape cet événement et remplisse le réducteur de dialogue afin de le montrer.

Mon doute vient quand ce dialogue se soumet.

  • Comment ce composant peut-il envoyer l'action appropriée en fonction de la première action envoyée?
  • Le créateur d'action doit-il gérer cette logique?
  • Pouvons-nous ajouter des actions à l'intérieur du réducteur?

modifier:

pour le rendre plus clair:

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

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

J'essaie donc de réutiliser le composant de dialogue. Afficher/masquer le dialogue n'est pas le problème car cela peut être facilement fait dans le réducteur. Ce que j'essaie de préciser, c'est comment répartir l'action depuis le côté droit en fonction de l'action qui démarre le flux dans le côté gauche.

208
carlesba

Vous trouverez ici beaucoup de bonnes solutions et de précieux commentaires d'experts connus de la communauté JS sur le sujet. Cela pourrait indiquer que ce n’est pas ce problème aussi trivial que cela puisse paraître. Je pense que c’est la raison pour laquelle cela pourrait être source de doutes et d’incertitudes sur la question.

Le problème fondamental ici est que dans React, vous ne pouvez monter le composant sur son parent, ce qui n’est pas toujours le comportement souhaité. Mais comment résoudre ce problème?

Je propose la solution, adressée pour résoudre ce problème. Vous trouverez une définition plus détaillée du problème, un code source de problème et des exemples à l'adresse suivante: https://github.com/fckt/react-layer-stack#rationale

Raisonnement

react/react-dom vient avec 2 hypothèses/idées de base:

  • chaque interface utilisateur est naturellement hiérarchique. C’est pourquoi nous avons l’idée de components qui s’enveloppent
  • react-dom monte le composant (physique) enfant sur son noeud DOM parent par défaut

Le problème est que, parfois, la deuxième propriété n'est pas ce que vous voulez. dans ton cas. Parfois, vous souhaitez monter votre composant dans nœud DOM physique différent et maintien de la connexion logique entre parent et enfant en même temps.

Un exemple canonique est un composant de type Tooltip: à un moment donné de processus de développement, vous pourriez trouver que vous devez ajouter un peu description de votre UI element: il sera rendu en couche fixe et devrait connaître ses coordonnées (qui sont ce UI element coord ou coords de la souris) et en même temps il a besoin d’informations pour savoir si doit être montré maintenant ou pas, son contenu et un contexte de composants parents. Cet exemple montre que parfois la hiérarchie logique ne correspond pas à la hiérarchie physique du DOM.

Jetez un coup d’œil à https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example pour voir l’exemple concret qui répond à votre question :

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>)
// ...
8
fckt

À mon avis, la mise en œuvre du strict minimum a deux exigences. Un état qui indique si le modal est ouvert ou non, et un portail pour rendre le modal en dehors de l'arborescence de réaction standard.

Le composant ModalContainer ci-dessous implémente ces exigences avec les fonctions de rendu correspondantes pour le modal et le déclencheur, qui est responsable de l'exécution du rappel pour ouvrir le modal.

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;

Et voici un cas d'utilisation simple ...

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;

J'utilise des fonctions de rendu, car je veux isoler la gestion de l'état et la logique standard de l'implémentation du composant modal et déclencheur rendu. Cela permet aux composants rendus d'être ce que vous voulez qu'ils soient. Dans votre cas, je suppose que le composant modal serait un composant connecté qui reçoit une fonction de rappel qui distribue une action asynchrone. 

Si vous devez envoyer des accessoires dynamiques au composant modal à partir du composant déclencheur, ce qui, espérons-le, n'arrivera pas trop souvent, je recommande d'encapsuler ModalContainer avec un composant conteneur qui gère les accessoires dynamiques dans son propre état et améliore les méthodes de rendu d'origine telles que alors.

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;
1
kskkido

Enveloppez le modal dans un conteneur connecté et effectuez l'opération async ici. De cette façon, vous pouvez accéder à la répartition pour déclencher des actions, ainsi qu'au prop onClose. (Pour atteindre dispatch à partir des accessoires, faites pas passe mapDispatchToProps à connect.

class ModalConteiner 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);

L'application où le modal est rendu et son état de visibilité est défini:

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

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

  ...

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

}
0
gazdagergo