Come smontare, annullare il rendering o rimuovere un componente da se stesso in un messaggio di notifica React / Redux / Typescript


114

So che questa domanda è già stata posta un paio di volte, ma la maggior parte delle volte la soluzione è gestirla nel genitore, poiché il flusso di responsabilità è solo discendente. Tuttavia, a volte, è necessario eliminare un componente da uno dei suoi metodi. So di non poter modificare i suoi oggetti di scena e se comincio ad aggiungere booleani come stato, inizierà a essere davvero disordinato per un semplice componente. Ecco cosa sto cercando di ottenere: un piccolo componente della casella di errore, con una "x" per chiuderlo. La ricezione di un errore tramite i suoi oggetti di scena lo visualizzerà, ma mi piacerebbe un modo per chiuderlo dal suo codice.

class ErrorBoxComponent extends React.Component {

  dismiss() {
    // What should I put here?
  }
  
  render() {
    if (!this.props.error) {
      return null;
    }

    return (
      <div data-alert className="alert-box error-box">
        {this.props.error}
        <a href="#" className="close" onClick={this.dismiss.bind(this)}>&times;</a>
      </div>
    );
  }
}


export default ErrorBoxComponent;

E lo userei in questo modo nel componente genitore:

<ErrorBox error={this.state.error}/>

Nella sezione Cosa devo mettere qui? , Ho già provato:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode); Il che genera un bel errore nella console:

Attenzione: unmountComponentAtNode (): il nodo che stai tentando di smontare è stato visualizzato da React e non è un contenitore di primo livello. Invece, chiedi al componente padre di aggiornare il proprio stato e di eseguire nuovamente il rendering per rimuovere questo componente.

Devo copiare gli oggetti di scena in arrivo nello stato ErrorBox e manipolarli solo internamente?


Stai usando Redux?
Arnau Lacambra

Perché questo è un requisito "La ricezione di un errore tramite i suoi oggetti di scena lo visualizzerà ma mi piacerebbe un modo per chiuderlo dal suo codice"? L'approccio normale sarebbe inviare un'azione che cancella lo stato di errore e poi si chiude in un ciclo di rendering del genitore come hai accennato.
ken4z

Vorrei offrire la possibilità per entrambi in realtà. Infatti, sarà chiudibile come lo hai spiegato, ma il mio caso è "e se volessi anche essere in grado di chiuderlo dall'interno"
Sephy

Risposte:


97

Proprio come quel simpatico avvertimento che hai ricevuto, stai cercando di fare qualcosa che è un Anti-Pattern in React. Questo è un no-no. React ha lo scopo di avere uno smontaggio da una relazione genitore a figlio. Ora, se vuoi che un bambino si smonti da solo, puoi simulare questo con un cambiamento di stato nel genitore che viene attivato dal bambino. lascia che te lo mostri in codice.

class Child extends React.Component {
    constructor(){}
    dismiss() {
        this.props.unmountMe();
    } 
    render(){
        // code
    }
}

class Parent ...
    constructor(){
        super(props)
        this.state = {renderChild: true};
        this.handleChildUnmount = this.handleChildUnmount.bind(this);
    }
    handleChildUnmount(){
        this.setState({renderChild: false});
    }
    render(){
        // code
        {this.state.renderChild ? <Child unmountMe={this.handleChildUnmount} /> : null}
    }

}

questo è un esempio molto semplice. ma puoi vedere un modo approssimativo per trasmettere un'azione al genitore

Detto questo, probabilmente dovresti passare attraverso il negozio (azione di spedizione) per consentire al tuo negozio di contenere i dati corretti quando va per il rendering

Ho inviato messaggi di errore / stato per due applicazioni separate, entrambe passate attraverso il negozio. È il metodo preferito ... Se lo desideri, posso pubblicare del codice su come farlo.

EDIT: Ecco come imposto un sistema di notifica usando React / Redux / Typescript

Poche cose da notare prima. questo è in dattiloscritto quindi dovresti rimuovere le dichiarazioni del tipo :)

Sto usando i pacchetti npm lodash per le operazioni e classnames (cx alias) per l'assegnazione inline classname.

La bellezza di questa configurazione è che utilizzo un identificatore univoco per ogni notifica quando l'azione la crea. (ad es. notify_id). Questo ID univoco è un file Symbol(). In questo modo, se desideri rimuovere qualsiasi notifica in qualsiasi momento, puoi farlo perché sai quale rimuovere. Questo sistema di notifica ti consentirà di impilarne quante ne vuoi e andranno via quando l'animazione sarà completata. Mi sto collegando all'evento di animazione e quando finisce faccio scattare un codice per rimuovere la notifica. Ho anche impostato un timeout di fallback per rimuovere la notifica nel caso in cui la richiamata dell'animazione non si attivi.

notifica-actions.ts

import { USER_SYSTEM_NOTIFICATION } from '../constants/action-types';

interface IDispatchType {
    type: string;
    payload?: any;
    remove?: Symbol;
}

export const notifySuccess = (message: any, duration?: number) => {
    return (dispatch: Function) => {
        dispatch({ type: USER_SYSTEM_NOTIFICATION, payload: { isSuccess: true, message, notify_id: Symbol(), duration } } as IDispatchType);
    };
};

export const notifyFailure = (message: any, duration?: number) => {
    return (dispatch: Function) => {
        dispatch({ type: USER_SYSTEM_NOTIFICATION, payload: { isSuccess: false, message, notify_id: Symbol(), duration } } as IDispatchType);
    };
};

export const clearNotification = (notifyId: Symbol) => {
    return (dispatch: Function) => {
        dispatch({ type: USER_SYSTEM_NOTIFICATION, remove: notifyId } as IDispatchType);
    };
};

notifica-reducer.ts

const defaultState = {
    userNotifications: []
};

export default (state: ISystemNotificationReducer = defaultState, action: IDispatchType) => {
    switch (action.type) {
        case USER_SYSTEM_NOTIFICATION:
            const list: ISystemNotification[] = _.clone(state.userNotifications) || [];
            if (_.has(action, 'remove')) {
                const key = parseInt(_.findKey(list, (n: ISystemNotification) => n.notify_id === action.remove));
                if (key) {
                    // mutate list and remove the specified item
                    list.splice(key, 1);
                }
            } else {
                list.push(action.payload);
            }
            return _.assign({}, state, { userNotifications: list });
    }
    return state;
};

app.tsx

nel rendering di base per la tua applicazione dovresti eseguire il rendering delle notifiche

render() {
    const { systemNotifications } = this.props;
    return (
        <div>
            <AppHeader />
            <div className="user-notify-wrap">
                { _.get(systemNotifications, 'userNotifications') && Boolean(_.get(systemNotifications, 'userNotifications.length'))
                    ? _.reverse(_.map(_.get(systemNotifications, 'userNotifications', []), (n, i) => <UserNotification key={i} data={n} clearNotification={this.props.actions.clearNotification} />))
                    : null
                }
            </div>
            <div className="content">
                {this.props.children}
            </div>
        </div>
    );
}

user-notification.tsx

classe di notifica utente

/*
    Simple notification class.

    Usage:
        <SomeComponent notifySuccess={this.props.notifySuccess} notifyFailure={this.props.notifyFailure} />
        these two functions are actions and should be props when the component is connect()ed

    call it with either a string or components. optional param of how long to display it (defaults to 5 seconds)
        this.props.notifySuccess('it Works!!!', 2);
        this.props.notifySuccess(<SomeComponentHere />, 15);
        this.props.notifyFailure(<div>You dun goofed</div>);

*/

interface IUserNotifyProps {
    data: any;
    clearNotification(notifyID: symbol): any;
}

export default class UserNotify extends React.Component<IUserNotifyProps, {}> {
    public notifyRef = null;
    private timeout = null;

    componentDidMount() {
        const duration: number = _.get(this.props, 'data.duration', '');
       
        this.notifyRef.style.animationDuration = duration ? `${duration}s` : '5s';

        
        // fallback incase the animation event doesn't fire
        const timeoutDuration = (duration * 1000) + 500;
        this.timeout = setTimeout(() => {
            this.notifyRef.classList.add('hidden');
            this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
        }, timeoutDuration);

        TransitionEvents.addEndEventListener(
            this.notifyRef,
            this.onAmimationComplete
        );
    }
    componentWillUnmount() {
        clearTimeout(this.timeout);

        TransitionEvents.removeEndEventListener(
            this.notifyRef,
            this.onAmimationComplete
        );
    }
    onAmimationComplete = (e) => {
        if (_.get(e, 'animationName') === 'fadeInAndOut') {
            this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
        }
    }
    handleCloseClick = (e) => {
        e.preventDefault();
        this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
    }
    assignNotifyRef = target => this.notifyRef = target;
    render() {
        const {data, clearNotification} = this.props;
        return (
            <div ref={this.assignNotifyRef} className={cx('user-notification fade-in-out', {success: data.isSuccess, failure: !data.isSuccess})}>
                {!_.isString(data.message) ? data.message : <h3>{data.message}</h3>}
                <div className="close-message" onClick={this.handleCloseClick}>+</div>
            </div>
        );
    }
}

1
"attraverso il negozio"? Penso, mi sto perdendo alcune lezioni cruciali su questo: D Grazie per la risposta e il codice ma non pensi che questo sia seriamente eccessivo per un semplice componente di visualizzazione dei messaggi di errore? Non dovrebbe essere responsabilità del genitore gestire un'azione definita sul bambino ...
Sephy

Dovrebbe essere il genitore in realtà poiché il genitore è responsabile di mettere il bambino nel DOM in primo luogo. Come stavo dicendo però, anche se questo è un modo per farlo, non lo consiglierei. Dovresti utilizzare un'azione che aggiorna il tuo negozio. entrambi i pattern Flux e Redux dovrebbero essere usati in questo modo.
John Ruddell

Ok allora, sarei felice di ricevere qualche puntatore di frammenti di codice, se lo desideri. Tornerò a quel pezzo di codice quando avrò letto qualcosa su Flux e Reduc!
Sephy

Ok sì, penso che farò un semplice repository GitHub che mostra un modo per farlo. L'ultimo che ho fatto ho usato le animazioni CSS per sfumare in dissolvenza l'elemento che poteva rendere gli elementi stringa o html e poi quando l'animazione è stata completata ho usato javascript per ascoltarlo e poi si pulisce (rimuovi dal DOM) quando l'animazione è terminata o hai fatto clic sul pulsante per eliminare.
John Ruddell

Per favore fallo, se può aiutare altri come me che faticano un po 'a comprendere la filosofia di React. Inoltre, sarei lieto di separarmi da un po 'dei miei punti per il tempo impiegato Se metti su un repo git per questo! Diciamo cento punti (taglia disponibile in 2 giorni però)
Sephy

25

invece di usare

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);

prova a usare

ReactDOM.unmountComponentAtNode(document.getElementById('root'));

Qualcuno l'ha provato con React 15? Questo sembra sia potenzialmente utile che forse un anti-pattern.
theUtherSide

4
@theUtherSide questo è un anti pattern in react. I documenti di React consigliano di smontare un bambino dal genitore tramite state / props
John Ruddell,

1
Cosa succede se il componente che viene smontato è la radice della tua app React ma non l'elemento radice che viene sostituito? Ad esempio <div id="c1"><div id="c2"><div id="react-root" /></div></div>. E se il testo interno di c1viene sostituito?
flipdoubt

1
Ciò è utile se desideri smontare il tuo componente root, specialmente se hai un'app React che risiede in un'app non-React. Ho dovuto usarlo perché volevo rendere la reazione all'interno di un modale gestito da un'altra app, e il loro modale ha pulsanti di chiusura che nascondono il modale ma il mio Reactdom rimarrà comunque montato. reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html
Abba

10

Nella maggior parte dei casi, è sufficiente nascondere l'elemento, ad esempio in questo modo:

export default class ErrorBoxComponent extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            isHidden: false
        }
    }

    dismiss() {
        this.setState({
            isHidden: true
        })
    }

    render() {
        if (!this.props.error) {
            return null;
        }

        return (
            <div data-alert className={ "alert-box error-box " + (this.state.isHidden ? 'DISPLAY-NONE-CLASS' : '') }>
                { this.props.error }
                <a href="#" className="close" onClick={ this.dismiss.bind(this) }>&times;</a>
            </div>
        );
    }
}

Oppure puoi eseguire il rendering / rieseguire / non eseguire il rendering tramite il componente principale in questo modo

export default class ParentComponent extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            isErrorShown: true
        }
    }

    dismiss() {
        this.setState({
            isErrorShown: false
        })
    }

    showError() {
        if (this.state.isErrorShown) {
            return <ErrorBox 
                error={ this.state.error }
                dismiss={ this.dismiss.bind(this) }
            />
        }

        return null;
    }

    render() {

        return (
            <div>
                { this.showError() }
            </div>
        );
    }
}

export default class ErrorBoxComponent extends React.Component {
    dismiss() {
        this.props.dismiss();
    }

    render() {
        if (!this.props.error) {
            return null;
        }

        return (
            <div data-alert className="alert-box error-box">
                { this.props.error }
                <a href="#" className="close" onClick={ this.dismiss.bind(this) }>&times;</a>
            </div>
        );
    }
}

Infine, c'è un modo per rimuovere il nodo html, ma davvero non so se sia una buona idea. Forse qualcuno che conosce React dall'interno dirà qualcosa al riguardo.

export default class ErrorBoxComponent extends React.Component {
    dismiss() {
        this.el.remove();
    }

    render() {
        if (!this.props.error) {
            return null;
        }

        return (
            <div data-alert className="alert-box error-box" ref={ (el) => { this.el = el} }>
                { this.props.error }
                <a href="#" className="close" onClick={ this.dismiss.bind(this) }>&times;</a>
            </div>
        );
    }
}

Ma, nel caso in cui voglio smontare un figlio che si trova all'interno di una lista di figli ... Cosa posso fare se voglio sostituire un componente clonato con la stessa chiave in quella lista?
roadev

1
da quanto ho capito che vuoi fare qualcosa del genere: document.getElementById (CHILD_NODE_ID) -> .remove (); -> document.getElementById (PARENT_NODE_ID) -> .appendChild (NEW_NODE)? Ho ragione? Dimenticalo. NON è un approccio di reazione. Usa lo stato del componente per il rendering delle condizioni
Sasha Kos

2

Sono stato a questo post circa 10 volte e volevo solo lasciare i miei due centesimi qui. Puoi semplicemente smontarlo in modo condizionale.

if (renderMyComponent) {
  <MyComponent props={...} />
}

Tutto quello che devi fare è rimuoverlo dal DOM per smontarlo.

Finché renderMyComponent = true, il componente verrà renderizzato. Se lo imposti renderMyComponent = false, verrà smontato dal DOM.


-1

Questo non è appropriato in tutte le situazioni, ma puoi condizionatamente return falseall'interno del componente stesso se un determinato criterio è o non è soddisfatto.

Non smonta il componente, ma rimuove tutto il contenuto renderizzato. Questo sarebbe solo un male, a mio avviso, se hai listener di eventi nel componente che dovrebbero essere rimossi quando il componente non è più necessario.

import React, { Component } from 'react';

export default class MyComponent extends Component {
    constructor(props) {
        super(props);

        this.state = {
            hideComponent: false
        }
    }

    closeThis = () => {
        this.setState(prevState => ({
            hideComponent: !prevState.hideComponent
        })
    });

    render() {
        if (this.state.hideComponent === true) {return false;}

        return (
            <div className={`content`} onClick={() => this.closeThis}>
                YOUR CODE HERE
            </div>
        );
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.