Come posso visualizzare una window di dialogo modale in Redux che esegue azioni asincrone?

Sto costruendo un'applicazione che deve mostrare una window di conferma in alcune situazioni.

Diciamo che voglio rimuovere qualcosa, quindi deleteSomething(id) un'azione come deleteSomething(id) modo che un riduttore prenderà l'evento e riempirà il riduttore di dialogo per mostrarlo.

Il mio dubbio arriva quando questa window di dialogo presenta.

  • Come può questo componente submit l'azione corretta secondo la prima azione inviata?
  • Dovrebbe il creatore dell'azione gestire questa logica?
  • Possiamo aggiungere azioni all'interno del riduttore?

edit:

per renderla più chiara:

 deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id) createThingB(id) => Show dialog with Questions => createThingBRemotely(id) 

Quindi sto cercando di riutilizzare la componente di dialogo. Visualizzare / hide la window di dialogo non è il problema poiché questo può essere facilmente fatto nel riduttore. Quello che sto cercando di specificare è come submit l'azione dal lato destro in base all'azione che inizia il stream nel lato sinistro.

L'approccio che suggerisco è un po 'verboso, ma ho trovato scalare abbastanza bene in applicazioni complesse. Quando desideri mostrare un modale, spara un'azione che descrive quale modale vorresti vedere:

Spedire un'azione per mostrare il Modal

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

(Le stringhe possono essere costanti ovviamente, sto usando stringhe inline per semplicità.)

Scrivere un riduttore per gestire lo stato modale

Quindi assicuratevi di avere un riduttore che accetta solo questi valori:

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

Grande! Ora, quando invii un'azione, state.modal aggiornerà per includere le informazioni sulla window modale attualmente visibile.

Scrivere il componente modale radice

Alla base della tua gerarchia di componenti, aggiungere un componente <ModalRoot> connesso al negozio Redux. state.modal e visualizzerà una componente modale appropriata, inoltrando i puntelli dallo 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) 

Cosa abbiamo fatto qui? ModalRoot legge l'attuale modalType e modalProps da state.modal a cui è connesso e rende un componente corrispondente come DeletePostModal o ConfirmLogoutModal . Ogni modale è un componente!

Scrivere componenti modali specifici

Non ci sono regole generali qui. Sono solo componenti di React che possono submit azioni, leggere qualcosa dallo stato del negozio e sono solo modali .

Ad esempio, DeletePostModal potrebbe essere simile a:

 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 è connesso al negozio in modo da poter visualizzare il titolo del post e funziona come qualsiasi altro componente collegato: può spedire azioni, incluse hideModal quando è necessario nascondersi.

Estrazione di una componente di presentazione

Sarebbe scomodo copiare-incollare la stessa logica di layout per each modal "specifico". Ma hai componenti, giusto? Quindi è ansible estrarre un componente di presentazione <Modal> che non conosce quali modalità modali fanno, ma gestisce come si guardano.

Quindi, modalità specifiche come DeletePostModal possono utilizzarlo per rendere:

 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) 

Spetta a te di trovare una serie di oggetti che <Modal> possono accettare nella tua applicazione, ma immagino che tu possa avere diversi tipi di modali (ad es. Modal info, modalità di conferma, ecc.) E diversi stili per loro .

Accessibilità e Nascondimento su Click Outside o Escape Key

L'ultima parte importnte delle modalità è che generalmente vogliamo nasconderle quando l'utente scatta all'esterno o preme Escape.

Invece di darti consigli sull'attuazione di ciò, suggerisco di non eseguirlo da soli. È difficile arrivare a destra considerando l'accessibilità.

Al contrario, vi suggerisco di utilizzare un componente modale accessibile in modalità off-the-shelf, come react-modal . È completamente personalizzabile, puoi mettere tutto ciò che vuoi all'interno di esso, ma gestisce l'accessibilità correttamente in modo che i ciechi possano ancora utilizzare il tuo modale.

È ansible anche avvolgere il react-modal nel tuo <Modal> che accetta puntelli specifici per le tue applicazioni e genera pulsanti per bambini o altri contenuti. Sono tutti solo componenti!

Altri approcci

Esiste più di un modo per farlo.

Alcune persone non amano la verbosità di questo approccio e preferiscono avere un componente <Modal> che possano rendere al proprio interno i loro componenti con una tecnica chiamata "portli". I portli ti permettono di rendere un componente all'interno del tuo mentre effettivamente renderà in un luogo predeterminato nel DOM, che è molto conveniente per le modalita '.

Infatti il react-modal ho collegato in precedenza già lo fa internamente in modo tecnicamente non è neanche necessario renderlo dall'alto. Mi sembra comunque bello separare il modale che voglio mostrare dal componente che lo mostra, ma puoi anche utilizzare il react-modal direttamente dai tuoi componenti e ignorare la maggior parte di ciò che ho scritto sopra.

Vi incoraggio a prendere in considerazione entrambi gli approcci, sperimentarli e scegliere quello che trovi per meglio funzionare per la tua applicazione e per la tua squadra.

Usa i portli

Dan Abbranchv risponde prima parte è soddisfacente, ma coinvolge un sacco di boilerplate. Come ha detto, puoi anche utilizzare i portli. Mi espanderò un po 'su quella idea.

Il vantaggio di un portle è che il popup e il button rimangono molto vicini all'tree React, con una comunicazione molto semplice di padre / figlio utilizzando puntelli: è ansible gestire facilmente le azioni async con i portli o lasciare che il padre personalizza il portle.

Che cosa è un portle?

Un portle consente di rendere direttamente all'interno di document.body un elemento che è profondamente annidato nel tuo tree React.

L'idea è che ad esempio si renda in corpo il seguente tree React:

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

E ottieni come output:

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

Il nodo del inside-portl è stato tradotto all'interno di <body> , anziché il suo luogo normale e profondamente nidificato.

Quando utilizzare un portle

Un portle è particolarmente utile per visualizzare elementi che dovrebbero andare al di là dei componenti esistenti di React: popup, dropdown, suggerimenti, hotspot

Perché utilizzare un portle

Nessun problema di z-index più : un portle consente di rendere <body> . Se si desidera visualizzare un popup o un menu a discesa, questa è un'idea veramente bella se non si desidera combattere contro i problemi dell'indice z. Gli elementi del portle si aggiungono a document.body nell'ordine di assembly, il che significa che se non si gioca con z-index , il comportmento predefinito sarà quello di raggruppare i portli in cima all'altro, in ordine di assembly. In pratica, significa che è ansible aprire un popup in modo sicuro da un altro popup e assicurarsi che il secondo popup verrà visualizzato in cima al primo senza wherer pensare a z-index .

In pratica

Più semplici: utilizza lo stato di React locale: se pensi, per un semplice popup di conferma delle eliminazioni, non vale la pena di avere il boiler di Redux, allora puoi usare un portle e semplifica notevolmente il tuo codice. Per tali usecas, where l'interazione è molto locale e in realtà è un vero e proprio dettaglio di implementazione, ti interessa veramente la ricarica a caldo, il viaggio in tempo, la logging di azioni e tutti i vantaggi che Redux ti port? Personalmente non faccio e non uso lo stato locale in questo caso. Il codice diventa semplice come:

 const DeleteButton = React.createClass({ propTypes: { onDelete: React.PropTypes.func.isRequired, } getInitialState() { return {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> ); } }) 

Semplice: puoi ancora utilizzare lo stato Redux : se vuoi veramente, puoi ancora utilizzare la connect per scegliere se visualizzare o less la DeleteConfirmationPopup . Poiché il portle rimane profondamente annidato nel tuo tree React, è molto semplice personalizzare il comportmento di questo portle perché il tuo genitore può passare i puntelli al portle. Se non si utilizzano i portli, di solito è necessario rendere i tuoi popup alla parte superiore dell'tree React per motivi di z-index e di solito dovrebbero pensare a pensare come "come personalizzare il generico DeleteConfirmationPopup I costruito in base all'utilizzo caso". Di solito troverai soluzioni abbastanza ingannevoli a questo problema, come spedire un'azione che contiene azioni di conferma / cancellazione nidificate, una chiave di traduzione o, peggio ancora, una function di rendering (o qualcos'altro non potenziata). Non devi farlo con i portli e puoi passare regolarmente i puntelli, poiché DeleteConfirmationPopup è solo un figlio del DeleteButton

Conclusione

I portli sono molto utili per semplificare il codice. Non potevo più farlo senza di loro.

Tieni presente che le implementazioni del portle possono anche aiutarti con altre funzionalità utili come:

  • Accessibilità
  • Scorciatoie Espace per chiudere il portle
  • Maneggiare fuori clic (chiudi il portle o no)
  • Mantieni il link di collegamento (chiudi il portle o no)
  • React Context disponibile nell'tree del portle

react-portl o react-modal sono piacevoli per i popup, le modalità e le sovrapposizioni che dovrebbero essere a schermo integer, generalmente centrate al centro dello schermo.

React -tether è sconosciuto dalla maggior parte degli sviluppatori di React, ma è uno degli strumenti più utili da scoprire. Tether ti consente di creare portli, ma posizionerà automaticamente il portle, rispetto ad un determinato target. Questo è perfetto per i suggerimenti, i menu a discesa, i punti di forza, i helpbox … Se hai mai avuto problemi con la posizione absolute / relative e z-index o il tuo dropdown che esce dal tuo viewport, Tether risolverà tutto ciò per te.

Ad esempio, è ansible implementare facilmente gli hotspot in bordo, che si espandono a un tooltip una volta cliccato:

A bordo di hotspot

Codice di produzione reale qui. Non può essere più semplice 🙂

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

Modifica : ha appena scoperto il gateway di reazione che consente di rendere i portli nel nodo di tua scelta (non necessariamente il corpo)

Modifica : sembra reactjs-popper può essere un'alternativa decente per reactjs-tether. PopperJS è un lib che calcola solo una posizione appropriata per un elemento, senza toccare direttamente il DOM, lasciando all'utente scegliere where e quando vuole mettere il nodo DOM, mentre Tether si aggiunge direttamente al corpo.

Modifica : le versioni successive di React (Fiber: probabilmente 16 o 17) includeranno un metodo per creare i portli: ReactDOM.unstable_createPortal() link

Potrebbero essere trovate molte buone soluzioni e commenti importnti da esperti esperti della comunità JS sull'argomento. Potrebbe essere un indicatore che non è un problema banale come potrebbe sembrare. Penso che questo sia il motivo per cui potrebbe essere la fonte di dubbi e incertezze sulla questione.

Il problema fondamentale qui è che in React è consentito solo di montare componente al suo genitore, che non è sempre il comportmento desiderato. Ma come affrontare questo problema?

Propongo la soluzione, indirizzata a risolvere questo problema. Ulteriori dettagli di definizione del problema, src ed esempi possono essere trovati qui: https://github.com/fckt/react-layer-stack#raftale

Fondamento logico

react / react-dom viene con 2 ipotesi / idee di base:

  • each UI è gerarchico naturalmente. Per questo abbiamo l'idea di components che si avvolgono tra loro
  • react-dom monta (fisicamente) il componente figlio al suo nodo principale DOM per impostazione predefinita

Il problema è che a volte la seconda properties; non è ciò che vuoi nel tuo caso. A volte si desidera montare il componente in un nodo fisico DOM diverso e mantenere la connessione logica tra genitore e figlio contemporaneamente.

L'esempio Canonical è un componente simile a Tooltip: in un certo punto del process di sviluppo è ansible trovare che è necessario aggiungere una descrizione per l' UI element : verrà eseguito in un livello fisso e dovrebbe conoscere le sue coordinate (che sono quelle UI element o mouse coords) e al tempo stesso ha bisogno di informazioni se deve essere mostrato adesso o no, il suo contenuto e un context da parte dei componenti genitori. Questo esempio mostra che talvolta la gerarchia logica non corrisponde alla gerarchia DOM fisica.

Date un'occhiata a https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example per vedere l'esempio concreto che risponde alla tua domanda:

 import { Layer, LayerContext } from 'react-layer-stack' // ... for each `object` in arrays 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>) // ...