it-swarm.com.de

Wie kann ich in Redux ein modales Dialogfeld anzeigen, das asynchrone Aktionen ausführt?

Ich baue eine App, die in einigen Situationen ein Bestätigungsdialogfeld anzeigen muss.

Nehmen wir an, ich möchte etwas entfernen, dann schicke ich eine Aktion wie deleteSomething(id) ab, damit ein Reducer das Ereignis fängt und den Dialog-Reducer füllt, um es anzuzeigen.

Ich bezweifle, dass sich dieser Dialog ergibt.

  • Wie kann diese Komponente die richtige Aktion gemäß der ersten ausgeführten Aktion ausführen?
  • Soll der Aktionsersteller mit dieser Logik umgehen?
  • Können wir Aktionen innerhalb des Reduzierers hinzufügen?

bearbeiten:

um es klarer zu machen:

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

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

Ich versuche also, die Dialogkomponente wiederzuverwenden. Das Anzeigen/Ausblenden des Dialogs ist nicht das Problem, da dies im Reducer leicht gemacht werden kann. Was ich versuche zu spezifizieren, ist, wie die Aktion von der rechten Seite abgesetzt wird, entsprechend der Aktion, die den Fluss auf der linken Seite startet.

208
carlesba

Der Ansatz, den ich vorschlage, ist ein wenig ausführlich, aber ich fand es gut in komplexe Apps zu skalieren. Wenn Sie einen Modal anzeigen möchten, lösen Sie eine Aktion aus, in der welche-Modal beschrieben wird:

Aktion auslösen, um den Modal anzuzeigen

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

(Strings können natürlich Konstanten sein; ich verwende zur Vereinfachung Inline-Strings.)

Reducer schreiben, um den modalen Status zu verwalten

Stellen Sie dann sicher, dass Sie einen Reduzierer haben, der diese Werte akzeptiert:

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 */
})

Großartig! Wenn Sie nun eine Aktion auslösen, wird state.modal aktualisiert, um die Informationen zum aktuell sichtbaren modalen Fenster einzuschließen.

Die modale Wurzelkomponente schreiben

Fügen Sie im Stammverzeichnis Ihrer Komponentenhierarchie eine <ModalRoot>-Komponente hinzu, die mit dem Redux-Store verbunden ist. Es hört auf state.modal und zeigt eine entsprechende modale Komponente an, wobei die Requisiten vom state.modal.modalProps weitergeleitet werden.

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

Was haben wir hier gemacht? ModalRoot liest die aktuelle modalType und modalProps aus state.modal, an die sie angeschlossen ist, und rendert eine entsprechende Komponente wie DeletePostModal oder ConfirmLogoutModal. Jeder Modal ist eine Komponente!

Spezifische modale Komponenten schreiben

Hier gibt es keine allgemeinen Regeln. Sie sind nur React-Komponenten, die Aktionen auslösen können, etwas aus dem Speicherstatus lesen und sind und zufällig Modals sind.

Zum Beispiel könnte DeletePostModal so aussehen:

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)

Die Variable DeletePostModal ist mit dem Geschäft verbunden, sodass sie den Posttitel anzeigen kann und wie jede verbundene Komponente funktioniert: Sie kann Aktionen auslösen, einschließlich hideModal, wenn es notwendig ist, sich selbst auszublenden.

Extrahieren einer Präsentationskomponente

Es wäre umständlich, für jedes "spezifische" Modal die gleiche Layout-Logik zu kopieren und einzufügen. Aber Sie haben Komponenten, richtig? Sie können also eine presentational<Modal>-Komponente extrahieren, die nicht weiß, was bestimmte Modale tun, aber wie sie aussehen soll.

Dann können bestimmte Modals wie DeletePostModal es für das Rendern verwenden:

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)

Es liegt an Ihnen, eine Reihe von Requisiten auszuarbeiten, die <Modal> in Ihrer Anwendung akzeptieren kann, aber ich könnte mir vorstellen, dass Sie verschiedene Modalarten (z. B. Info-Modal, Bestätigungs-Modal usw.) und verschiedene Stile dafür haben.

Eingabehilfen und Ausblenden beim Klicken außerhalb oder Escape-Taste

Der letzte wichtige Aspekt von Modalen ist, dass wir sie im Allgemeinen ausblenden möchten, wenn der Benutzer nach außen klickt oder die Escape-Taste drückt.

Anstatt Ihnen Ratschläge zu geben, schlage ich vor, dass Sie es nicht selbst implementieren. In Anbetracht der Barrierefreiheit ist es schwierig, richtig zu werden.

Stattdessen würde ich Ihnen empfehlen, eine modulare Komponente von zugänglich zu verwenden, z. B. react-modal . Es ist vollständig anpassbar. Sie können alles, was Sie möchten, darin einfügen, aber es funktioniert korrekt mit der Zugänglichkeit, so dass Blinde Ihren Modal weiterhin verwenden können.

Sie können react-modal sogar in Ihren eigenen <Modal> einwickeln, der die für Ihre Anwendungen spezifischen Requisiten akzeptiert und untergeordnete Schaltflächen oder anderen Inhalt generiert. Es sind alles nur Komponenten!

Andere Ansätze

Es gibt mehr als eine Möglichkeit, dies zu tun.

Einige Leute mögen die Ausführlichkeit dieses Ansatzes nicht und bevorzugen es, eine <Modal>-Komponente zu haben, die sie direkt in ihren Komponenten mit einer als "Portale" bezeichneten Technik darstellen können. Mit Portalen können Sie eine Komponente in Ihrem darstellen, während tatsächlich an einer vorbestimmten Stelle im DOM gerendert wird, was für Modale sehr praktisch ist.

Tatsächlich react-modal Ich habe schon früher mit einem Link zu tun, tut dies intern. Technisch gesehen müssen Sie es nicht einmal von oben darstellen. Ich finde es immer noch schön, die Modalität, die ich zeigen möchte, von der Komponente zu entkoppeln, die sie anzeigt, aber Sie können auch react-modal direkt von Ihren Komponenten verwenden und die meisten der oben genannten Informationen überspringen.

Ich möchte Sie dazu ermutigen, beide Ansätze in Betracht zu ziehen, mit ihnen zu experimentieren und auszuwählen, was für Ihre App und Ihr Team am besten geeignet ist.

453
Dan Abramov

Update : React 16.0 führte Portale durch ReactDOM.createPortalLink ein

Update : Die nächsten Versionen von React (Fibre: wahrscheinlich 16 oder 17) enthalten eine Methode zum Erstellen von Portalen: ReactDOM.unstable_createPortal()link


Verwenden Sie Portale

Dan Abramovs Antwort auf den ersten Teil ist in Ordnung, beinhaltet aber eine Menge Boilerplate. Wie er sagte, können Sie auch Portale benutzen. Ich werde auf diese Idee etwas näher eingehen.

Der Vorteil eines Portals besteht darin, dass das Popup und die Schaltfläche in der React -Baumstruktur sehr nahe beieinander liegen, wobei die Kommunikation zwischen Eltern und Kindern mithilfe von Requisiten sehr einfach ist: Sie können problemlos asynchrone Aktionen mit Portalen ausführen oder die Eltern anpassen das Portal.

Was ist ein Portal?

Über ein Portal können Sie direkt in document.body Ein Element rendern, das tief in Ihrem React -Baum verschachtelt ist.

Die Idee ist, dass Sie zum Beispiel den folgenden React -Baum in body rendern:

<div className="layout">
  <div className="outside-portal">
    <Portal>
      <div className="inside-portal">
        PortalContent
      </div>
    </Portal>
  </div>
</div>

Und Sie erhalten als Ausgabe:

<body>
  <div class="layout">
    <div class="outside-portal">
    </div>
  </div>
  <div class="inside-portal">
    PortalContent
  </div>
</body>

Der Knoten inside-portal Wurde innerhalb von <body> Anstelle seines normalen, tief verschachtelten Platzes übersetzt.

Wann soll ein Portal benutzt werden?

Ein Portal ist besonders hilfreich für die Anzeige von Elementen, die über Ihre vorhandenen React Komponenten gestellt werden sollen: Popups, Dropdowns, Vorschläge, Hotspots

Warum ein Portal benutzen?

Keine Z-Index-Probleme mehr : Ein Portal erlaubt es Ihnen, nach <body> Zu rendern. Wenn Sie ein Popup oder eine Dropdown-Liste anzeigen möchten, ist dies eine wirklich gute Idee, wenn Sie nicht gegen Z-Index-Probleme kämpfen müssen. Die hinzugefügten Portal-Elemente führen document.body In der Reihenfolge des Einbindens aus. Wenn Sie also nicht mit z-index Spielen, werden die Portale standardmäßig in der Reihenfolge des Einbindens übereinander gestapelt. In der Praxis bedeutet dies, dass Sie ein Popup sicher von einem anderen Popup aus öffnen und sicherstellen können, dass das zweite Popup über dem ersten angezeigt wird, ohne dass Sie sich auch nur Gedanken über z-index Machen müssen.

In der Praxis

Am einfachsten: Verwenden Sie local React state: Wenn Sie der Meinung sind, dass es für ein einfaches Bestätigungs-Popup zum Löschen nicht wert ist, den Redux zu haben Boilerplate, dann können Sie ein Portal verwenden und es vereinfacht Ihren Code erheblich. Interessieren Sie sich für einen solchen Anwendungsfall, in dem die Interaktion sehr lokal ist und tatsächlich ein ziemlich detailliertes Implementierungsdetail darstellt, wirklich für Hot-Reloading, Zeitreisen, Aktionsprotokollierung und alle Vorteile, die Ihnen Redux bietet? Persönlich verwende ich in diesem Fall kein lokales Bundesland. Der Code wird so einfach wie:

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>
    );
  }
}

Einfach: Sie können immer noch den Redux-Status verwenden : Wenn Sie wirklich wollen, können Sie immer noch connect verwenden, um zu wählen, ob das DeleteConfirmationPopup wird angezeigt oder nicht. Da das Portal tief in Ihrem React -Stammbaum verschachtelt bleibt, ist es sehr einfach, das Verhalten dieses Portals anzupassen, da Ihre Eltern Requisiten an das Portal übergeben können. Wenn Sie keine Portale verwenden, müssen Sie Ihre Popups normalerweise aus z-index Gründen am oberen Rand Ihres React Baums rendern und in der Regel über Dinge wie "Wie passe ich sie an?" Nachdenken das generische DeleteConfirmationPopup, das ich je nach Anwendungsfall erstellt habe ". Und normalerweise finden Sie ziemlich hackige Lösungen für dieses Problem, wie das Auslösen einer Aktion, die verschachtelte Bestätigungs-/Abbruchaktionen enthält, eines Übersetzungspaketschlüssels oder, noch schlimmer, einer Renderfunktion (oder etwas anderem, was unserialisierbar ist). Sie müssen das nicht mit Portalen machen und können nur normale Requisiten übergeben, da DeleteConfirmationPopup nur ein Kind von DeleteButton ist.

Fazit

Portale sind sehr nützlich, um Ihren Code zu vereinfachen. Ich konnte nicht mehr ohne sie auskommen.

Beachten Sie, dass Portalimplementierungen Ihnen auch bei anderen nützlichen Funktionen helfen können, z.

  • Barrierefreiheit
  • Espace-Verknüpfungen zum Schließen des Portals
  • Handle outside click (Portal schließen oder nicht)
  • Handle link click (Portal schließen oder nicht)
  • Reaktionskontext im Portalbaum verfügbar gemacht

reagiere-portal oder reagiere-modal eignen sich für Popups, Modals und Overlays, die im Vollbildmodus angezeigt werden sollen und im Allgemeinen in der Mitte des Bildschirms zentriert sind.

react-tether ist den meisten React Entwicklern unbekannt, aber es ist eines der nützlichsten Tools, die Sie dort finden können. Tether ermöglicht das Erstellen von Portalen, positioniert das Portal jedoch automatisch relativ zu einem bestimmten Ziel. Dies ist perfekt für Tooltips, Dropdowns, Hotspots, Hilfefelder ... Wenn Sie jemals ein Problem mit Position absolute/relative und z-index Oder Ihrem Dropdown außerhalb von hatten In Ihrem Ansichtsfenster löst Tether all das für Sie.

Sie können zum Beispiel einfach Onboarding-Hotspots implementieren, die durch Klicken zu einem Tooltip erweitert werden:

Onboarding hotspot

Echter Seriencode hier. Einfacher geht es nicht :)

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

Bearbeiten : gerade entdeckt Reagieren-Gateway das erlaubt, Portale in den Knoten Ihrer Wahl zu rendern (nicht notwendigerweise Körper)

Bearbeiten : es scheint React-Popper kann eine anständige Alternative zu React-Tether sein. PopperJS ist eine Bibliothek, die nur eine geeignete Position für ein Element berechnet, ohne das DOM direkt zu berühren, und dem Benutzer die Wahl lässt, wo und wann er den DOM-Knoten platzieren möchte, während Tether direkt an den Körper angehängt wird.

Bearbeiten : Es gibt auch React-Slot-Fill , das interessant ist und ähnliche Probleme lösen kann, indem es erlaubt, ein Element zu rendern Ein reservierter Element-Slot, den Sie beliebig in Ihren Baum einfügen können

91

Hier finden Sie viele gute Lösungen und wertvolle Kommentare von bekannten Experten der JS-Community zum Thema. Es könnte ein Hinweis darauf sein, dass es nicht so trivial ist, wie es scheint. Ich denke, dass dies der Grund für Zweifel und Unsicherheit in dieser Angelegenheit sein kann.

Grundlegendes Problem hierbei ist, dass Sie in React nur Komponenten an das übergeordnete Element anhängen dürfen, was nicht immer das gewünschte Verhalten ist. Aber wie soll dieses Problem gelöst werden?

Ich schlage die Lösung vor, um dieses Problem zu beheben. Eine detailliertere Problemdefinition, Quelle und Beispiele finden Sie hier: https://github.com/fckt/react-layer-stack#rationale

Begründung

reactreact-dom kommt mit zwei grundlegenden Annahmen/Ideen:

Das Problem ist, dass manchmal die zweite Eigenschaft nicht Ihren Vorstellungen entspricht in Ihrem Fall. Manchmal möchten Sie Ihre Komponente in .__ einbinden. unterschiedlichen physischen DOM-Knoten und halten eine logische Verbindung zwischen Elternteil und Kind gleichzeitig.

Ein kanonisches Beispiel ist eine Tooltip-ähnliche Komponente: an einem Punkt von Entwicklungsprozess könnten Sie feststellen, dass Sie einige .__ hinzufügen müssen. Beschreibung für Ihren UI element: er wird in fester Ebene und .__ gerendert. sollte seine Koordinaten kennen (was UI element coord oder mouse coords ist) und gleichzeitig benötigt es Informationen, ob es muss jetzt oder nicht gezeigt werden, seinen Inhalt und einen Kontext aus übergeordnete Komponenten. Dieses Beispiel zeigt manchmal die logische Hierarchie stimmt nicht mit der physischen DOM-Hierarchie überein.

Schauen Sie sich _/https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example an, um das konkrete Beispiel zu sehen, das die Antwort auf Ihre Frage ist :

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

Meines Erachtens hat die minimale Implementierung zwei Anforderungen. Ein Status, der verfolgt, ob die Modalität offen ist oder nicht, und ein Portal, um die Modalität außerhalb des Standardreaktionsbaums darzustellen.

Die folgende ModalContainer-Komponente implementiert diese Anforderungen zusammen mit den entsprechenden Renderfunktionen für das Modal und den Trigger, der für die Ausführung des Callbacks zum Öffnen des Modal verantwortlich ist.

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;

Und hier ist ein einfacher Anwendungsfall ...

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;

Ich verwende Renderfunktionen, da ich die Zustandsverwaltung und die Boilerplate-Logik von der Implementierung der gerenderten Modal- und Trigger-Komponente trennen möchte. Auf diese Weise können die gerenderten Komponenten so sein, wie Sie es möchten. In Ihrem Fall gehe ich davon aus, dass die modale Komponente eine verbundene Komponente ist, die eine Rückruffunktion empfängt, die eine asynchrone Aktion auslöst. 

Wenn Sie dynamische Requisiten aus der Triggerkomponente an die modale Komponente senden müssen, was hoffentlich nicht allzu häufig vorkommt, empfehle ich, den ModalContainer mit einer Containerkomponente zu umschließen, die die dynamischen Requisiten in ihrem eigenen Zustand verwaltet und die ursprünglichen Rendermethoden verbessert so.

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

Wickeln Sie das Modal in einen verbundenen Container und führen Sie den asynchronen Vorgang hier aus. Auf diese Weise können Sie sowohl den Dispatch zum Auslösen von Aktionen als auch die Eigenschaft onClose erreichen. (Um dispatch von Requisiten zu erreichen, übergeben Sie die Funktion not mapDispatchToProps an 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);

Die App, in der das Modal gerendert wird und dessen Sichtbarkeitsstatus festgelegt ist:

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

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

  ...

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

}
0
gazdagergo