Cómo desmontar, deshacer o eliminar un componente de sí mismo en un mensaje de notificación de React / Redux / Typecript

114

Sé que esta pregunta ya se ha hecho un par de veces, pero la mayoría de las veces, la solución es manejar esto en el padre, ya que el flujo de responsabilidad solo es descendente. Sin embargo, a veces, es necesario eliminar un componente de uno de sus métodos. Sé que no puedo modificar sus accesorios, y si comienzo a agregar booleanos como estado, comenzará a ser realmente complicado para un componente simple. Esto es lo que estoy tratando de lograr: Un pequeño componente de cuadro de error, con una "x" para descartarlo. Recibir un error a través de sus accesorios lo mostrará, pero me gustaría una forma de cerrarlo desde su propio código.

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;

Y lo usaría así en el componente principal:

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

En la sección ¿Qué debo poner aquí? , Ya probé:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode); Lo que arroja un buen error en la consola:

Advertencia: unmountComponentAtNode (): React renderizó el nodo que estás intentando desmontar y no es un contenedor de nivel superior. En su lugar, haga que el componente principal actualice su estado y vuelva a procesarlo para eliminar este componente.

¿Debo copiar los accesorios entrantes en el estado ErrorBox y manipularlos solo internamente?

Sephy
fuente
¿Estás usando Redux?
Arnau Lacambra
¿Por qué es este un requisito "Recibir un error a través de sus accesorios lo mostrará, pero me gustaría una forma de cerrarlo desde su propio código"? El enfoque normal sería enviar una acción que borre el estado de error y luego se cierre en un ciclo de renderizado del padre como aludiste.
ken4z
En realidad, me gustaría ofrecer la posibilidad de ambos. De hecho, se podrá cerrar como lo explicó, pero mi caso es "¿y si también quiero poder cerrarlo desde el interior?"
Sephy

Respuestas:

97

Al igual que esa agradable advertencia que recibió, está tratando de hacer algo que sea un Anti-Patrón en React. Esto es un no-no. React tiene la intención de que suceda un desmontaje de una relación de padre a hijo. Ahora, si desea que un niño se desmonte solo, puede simular esto con un cambio de estado en el padre que es activado por el niño. déjame mostrarte en código.

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

}

este es un ejemplo muy simple. pero puede ver una forma aproximada de transmitirle al padre una acción

Dicho esto, probablemente debería pasar por la tienda (acción de envío) para permitir que su tienda contenga los datos correctos cuando se procese

He hecho mensajes de error / estado para dos aplicaciones separadas, ambas pasaron por la tienda. Es el método preferido ... Si lo desea, puedo publicar algún código sobre cómo hacerlo.

EDITAR: Así es como configuro un sistema de notificación usando React / Redux / Typescript

Pocas cosas para tener en cuenta primero. esto está en mecanografiado, por lo que necesitaría eliminar las declaraciones de tipo :)

Estoy usando los paquetes npm lodash para operaciones y nombres de clases (alias cx) para la asignación de nombres de clases en línea.

La belleza de esta configuración es que uso un identificador único para cada notificación cuando la acción la crea. (por ejemplo, notify_id). Esta identificación única es una Symbol(). De esta manera, si desea eliminar cualquier notificación en cualquier momento, puede hacerlo porque sabe cuál eliminar. Este sistema de notificación le permitirá apilar tantas como desee y desaparecerán cuando se complete la animación. Me estoy conectando al evento de animación y cuando finaliza, activo un código para eliminar la notificación. También configuré un tiempo de espera de respaldo para eliminar la notificación en caso de que la devolución de llamada de la animación no se active.

notificaciones-acciones.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);
    };
};

notificación-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

en el render base para su aplicación, renderizaría las notificaciones

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

clase de notificación de usuario

/*
    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
fuente
1
"a través de la tienda"? Creo que me estoy perdiendo algunas lecciones cruciales sobre eso: D Gracias por la respuesta y el código, pero ¿no crees que esto es demasiado para un componente de visualización de mensaje de error simple? No debería ser responsabilidad del padre manejar una acción definida en el niño ...
Sephy
En realidad, debería ser el padre, ya que el padre es responsable de poner al niño en el DOM en primer lugar. Sin embargo, como estaba diciendo, aunque esta es una forma de hacerlo, no la recomendaría. Debería utilizar una acción que actualice su tienda. tanto los patrones Flux como Redux deben usarse de esta manera.
John Ruddell
Ok, entonces, estaría feliz de obtener un puntero de fragmentos de código, por favor. ¡Volveré a ese fragmento de código cuando haya leído un poco sobre Flux y Reduc!
Sephy
Ok, sí, creo que haré un repositorio de github simple que muestre una forma de hacerlo. El último que hice, utilicé animaciones CSS para desvanecer el elemento que podría representar elementos de cadena o html y luego, cuando se completó la animación, usé javascript para escuchar eso y luego limpiarlo (eliminarlo del DOM) cuando el la animación terminó o hizo clic en el botón de descartar.
John Ruddell
Por favor, hágalo, si puede ayudar a otros como yo que luchan un poco por comprender la filosofía de React. Además, me alegraría separarme de un poco de mis puntos por el tiempo invertido. ¡Si pones un repositorio de git para esto! Digamos cien puntos (recompensa disponible en 2 días)
Sephy
25

En lugar de usar

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

intenta usar

ReactDOM.unmountComponentAtNode(document.getElementById('root'));
M Rezvani
fuente
¿Alguien ha probado esto con React 15? Esto parece tanto potencialmente útil como posiblemente un anti-patrón.
theUtherSide
4
@theUtherSide este es un patrón anti en reacción. React docs recomienda desmontar a un niño del padre a través de state / props
John Ruddell
1
¿Qué pasa si el componente que se está desmontando es la raíz de su aplicación React pero no el elemento raíz que se reemplaza? Por ejemplo <div id="c1"><div id="c2"><div id="react-root" /></div></div>. ¿Qué pasa si c1se reemplaza el texto interno de ?
flipdoubt
1
Esto es útil si desea desmontar su componente raíz, especialmente si tiene una aplicación de reacción que reside en una aplicación que no es de reacción. Tuve que usar esto porque quería renderizar react dentro de un modal manejado por otra aplicación, y su modal tiene botones de cierre que ocultarán el modal pero mi reactdom seguirá estando montado. reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html
Abba
10

En la mayoría de los casos, basta con ocultar el elemento, por ejemplo, de esta manera:

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

O puede renderizar / volver a renderizar / no renderizar a través de un componente principal como este

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

Finalmente, hay una forma de eliminar el nodo html, pero realmente no sé si es una buena idea. Tal vez alguien que conozca React from internal diga algo sobre esto.

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
fuente
Pero, en el caso de que quiera desmontar un niño que está dentro de una lista de niños ... ¿Qué puedo hacer si quiero reemplazar un componente clonado con la misma clave en esa lista?
roadev
1
según tengo entendido, quiere hacer algo como esto: document.getElementById (CHILD_NODE_ID) -> .remove (); -> document.getElementById (PARENT_NODE_ID) -> .appendChild (NEW_NODE)? Estoy en lo cierto? Olvídalo. NO es un enfoque de reacción. Utilice el estado del componente para la representación de condiciones
Sasha Kos
2

He estado en esta publicación unas 10 veces y solo quería dejar mis dos centavos aquí. Puede desmontarlo condicionalmente.

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

Todo lo que tienes que hacer es eliminarlo del DOM para poder desmontarlo.

Siempre que renderMyComponent = trueel componente se renderice. Si lo configura renderMyComponent = false, se desmontará del DOM.

ihodonald
fuente
-1

Esto no es apropiado en todas las situaciones, pero puede condicionalmente return falsedentro del propio componente si se cumplen o no ciertos criterios.

No desmonta el componente, pero elimina todo el contenido renderizado. Esto solo sería malo, en mi opinión, si tiene detectores de eventos en el componente que deberían eliminarse cuando el componente ya no sea necesario.

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