Comment démonter, annuler ou supprimer un composant de lui-même dans un message de notification React / Redux / Typescript

114

Je sais que cette question a déjà été posée à plusieurs reprises, mais la plupart du temps, la solution est de gérer cela dans le parent, car le flux de responsabilité ne fait que décroître. Cependant, parfois, vous devez supprimer un composant de l'une de ses méthodes. Je sais que je ne peux pas modifier ses accessoires, et si je commence à ajouter des booléens comme état, ça va commencer à être vraiment compliqué pour un composant simple. Voici ce que j'essaie de réaliser: Un petit composant de boîte d'erreur, avec un "x" pour le rejeter. Recevoir une erreur via ses accessoires l'affichera mais j'aimerais un moyen de le fermer à partir de son propre code.

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;

Et je l'utiliserais comme ceci dans le composant parent:

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

Dans la section Que dois-je mettre ici? , J'ai déjà essayé:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode); Ce qui jette une belle erreur dans la console:

Avertissement: unmountComponentAtNode (): le nœud que vous essayez de démonter a été rendu par React et n'est pas un conteneur de niveau supérieur. Au lieu de cela, demandez au composant parent de mettre à jour son état et le rendu afin de supprimer ce composant.

Dois-je copier les accessoires entrants dans l'état ErrorBox et les manipuler uniquement en interne?

Sephy
la source
Utilisez-vous Redux?
Arnau Lacambra
Pourquoi est-ce une exigence "Recevoir une erreur via ses accessoires l'affichera mais j'aimerais un moyen de le fermer à partir de son propre code."? L'approche normale consisterait à envoyer une action qui effacerait l'état d'erreur, puis se fermerait dans un cycle de rendu du parent comme vous l'avez évoqué.
ken4z
Je voudrais offrir la possibilité pour les deux en fait. En effet, il pourra être fermé comme vous l'avez expliqué, mais mon cas est "et si je veux aussi pouvoir le fermer de l'intérieur"
Sephy

Réponses:

97

Tout comme ce bel avertissement que vous avez reçu, vous essayez de faire quelque chose qui est un Anti-Pattern dans React. C'est un non-non. React est destiné à faire se produire un démontage d'une relation parent-enfant. Maintenant, si vous voulez qu'un enfant se démonte, vous pouvez simuler cela avec un changement d'état dans le parent qui est déclenché par l'enfant. laissez-moi vous montrer dans le code.

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

}

ceci est un exemple très simple. mais vous pouvez voir un moyen approximatif de transmettre au parent une action

Cela étant dit, vous devriez probablement passer par le magasin (action d'expédition) pour permettre à votre magasin de contenir les données correctes lors du rendu.

J'ai fait des messages d'erreur / d'état pour deux applications distinctes, les deux sont passées par le magasin. C'est la méthode préférée ... Si vous le souhaitez, je peux poster du code expliquant comment procéder.

EDIT: Voici comment j'ai mis en place un système de notification en utilisant React / Redux / Typescript

Peu de choses à noter en premier. c'est en typographie, vous devrez donc supprimer les déclarations de type :)

J'utilise les packages npm lodash pour les opérations et les noms de classe (alias cx) pour l'attribution de nom de classe en ligne.

La beauté de cette configuration est que j'utilise un identifiant unique pour chaque notification lorsque l'action la crée. (par exemple, notify_id). Cet identifiant unique est un Symbol(). De cette façon, si vous souhaitez supprimer une notification à tout moment, vous pouvez le faire, car vous savez laquelle supprimer. Ce système de notification vous permettra d'en empiler autant que vous le souhaitez et ils disparaîtront lorsque l'animation sera terminée. Je me connecte à l'événement d'animation et quand il se termine, je déclenche du code pour supprimer la notification. J'ai également mis en place un délai d'expiration de secours pour supprimer la notification au cas où le rappel d'animation ne se déclenche pas.

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

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

dans le rendu de base de votre application, vous rendriez les notifications

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 de notification utilisateur

/*
    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>
        );
    }
}
John Ruddell
la source
1
"à travers le magasin"? Je pense qu'il me manque quelques leçons cruciales à ce sujet: D Merci pour la réponse et le code, mais ne pensez-vous pas que c'est vraiment exagéré pour un simple composant d'affichage de message d'erreur? Il ne devrait pas être de la responsabilité du parent de gérer une action définie sur l'enfant ...
Sephy
Ce devrait être le parent en fait puisque le parent est responsable de mettre l'enfant dans le DOM en premier lieu. Comme je le disais, même si c'est une façon de le faire, je ne le recommanderais pas. Vous devriez utiliser une action qui met à jour votre boutique. les modèles Flux et Redux doivent être utilisés de cette façon.
John Ruddell
Ok alors, je serais ravi de recevoir un pointeur de fragments de code s'il vous plaît. Je reviendrai à ce morceau de code quand j'aurai lu un peu sur Flux et Reduc!
Sephy
Ok oui, je pense que je vais faire un simple repo github montrant un moyen de le faire. Le dernier que j'ai fait, j'ai utilisé des animations css pour faire fondre en fondu l'élément qui pouvait rendre des éléments de chaîne ou html, puis lorsque l'animation s'est terminée, j'ai utilisé javascript pour écouter cela, puis se nettoyer (supprimer du DOM) lorsque le l'animation est terminée ou vous avez cliqué sur le bouton de rejet.
John Ruddell
S'il vous plaît faites, si cela peut aider d'autres comme moi qui ont du mal à comprendre la philosophie de React. De plus, je serais heureux de me séparer d'un peu de mes points pour le temps pris Si vous mettez en place un dépôt git pour cela! Disons cent points (prime disponible dans 2 jours cependant)
Sephy
25

à la place d'utiliser

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

essayez d'utiliser

ReactDOM.unmountComponentAtNode(document.getElementById('root'));
M Rezvani
la source
Quelqu'un a-t-il essayé cela avec React 15? Cela semble à la fois potentiellement utile et peut-être un anti-pattern.
theUtherSide
4
@theUtherSide c'est un motif anti-réaction. React Docs vous recommande de démonter un enfant du parent via state / props
John Ruddell
1
Que faire si le composant à démonter est la racine de votre application React mais pas l'élément racine remplacé? Par exemple <div id="c1"><div id="c2"><div id="react-root" /></div></div>. Et si le texte interne de était c1remplacé?
flipdoubt
1
Ceci est utile si vous souhaitez démonter votre composant racine, en particulier si vous avez une application de réaction résidant dans une application sans réaction. J'ai dû l'utiliser parce que je voulais rendre réagir à l'intérieur d'un modal géré par une autre application, et leur modal a des boutons de fermeture qui masqueront le modal mais mon reactdom restera toujours monté. reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html
Abba
10

Dans la plupart des cas, il suffit de masquer l'élément, par exemple de cette manière:

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

Ou vous pouvez rendre / rendre / ne pas rendre via le composant parent comme celui-ci

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

Enfin, il existe un moyen de supprimer le nœud html, mais je ne sais vraiment pas si c'est une bonne idée. Peut-être que quelqu'un qui connaît React de l'intérieur dira quelque chose à ce sujet.

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>
        );
    }
}
Sasha Kos
la source
Mais, dans le cas où je veux démonter un enfant qui se trouve dans une liste d'enfants ... Que puis-je faire si je veux remplacer un composant cloné par la même clé dans cette liste?
roadev
1
si je comprends bien, vous voulez faire quelque chose comme ceci: document.getElementById (CHILD_NODE_ID) -> .remove (); -> document.getElementById (PARENT_NODE_ID) -> .appendChild (NEW_NODE)? Ai-je raison? Oublie ça. Ce n'est PAS une approche de réaction. Utiliser l'état du composant pour le rendu des conditions
Sasha Kos
2

Je suis allé à ce post environ 10 fois maintenant et je voulais juste laisser mes deux cents ici. Vous pouvez simplement le démonter sous condition.

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

Tout ce que vous avez à faire est de le supprimer du DOM pour le démonter.

Tant que renderMyComponent = true, le composant sera rendu. Si vous définissez renderMyComponent = false, il se démontera du DOM.

Ihodonald
la source
-1

Ce n'est pas approprié dans toutes les situations, mais vous pouvez conditionnellement return falseà l'intérieur du composant lui-même si un certain critère est satisfait ou non.

Il ne démonte pas le composant, mais supprime tout le contenu rendu. Ce ne serait mauvais, dans mon esprit, que si vous avez des écouteurs d'événements dans le composant qui devraient être supprimés lorsque le composant n'est plus nécessaire.

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>
        );
    }
}
nébuleuse
la source