Como desmontar, remover ou remover um componente de si mesmo em uma mensagem de notificação React / Redux / Typescript

114

Eu sei que esta pergunta já foi feita algumas vezes, mas na maioria das vezes, a solução é lidar com isso no pai, pois o fluxo de responsabilidade é apenas decrescente. No entanto, às vezes, você precisa eliminar um componente de um de seus métodos. Eu sei que não posso modificar seus adereços, e se eu começar a adicionar booleanos como o estado, vai começar a ficar muito confuso para um componente simples. Aqui está o que estou tentando alcançar: um pequeno componente de caixa de erro, com um "x" para descartá-lo. Receber um erro por meio de seus adereços irá exibi-lo, mas gostaria de uma forma de fechá-lo a partir de seu próprio 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;

E eu usaria assim no componente pai:

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

Na seção O que devo colocar aqui? , Já tentei:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode); O que gera um bom erro no console:

Aviso: unmountComponentAtNode (): o nó que você está tentando desmontar foi renderizado pelo React e não é um contêiner de nível superior. Em vez disso, faça com que o componente pai atualize seu estado e processe novamente para remover esse componente.

Devo copiar os adereços de entrada no estado ErrorBox e manipulá-los apenas internamente?

Sephy
fonte
Você está usando Redux?
Arnau Lacambra
Por que isso é um requisito "Receber um erro por meio de seus adereços irá exibi-lo, mas gostaria de uma forma de fechá-lo a partir de seu próprio código."? A abordagem normal seria despachar uma ação que limparia o estado de erro e, em seguida, seria fechada em um ciclo de renderização do pai, como você aludiu.
ken4z
Eu gostaria de oferecer a possibilidade para ambos, na verdade. Na verdade, ele poderá ser fechado conforme você explicou, mas meu caso é "e se eu também quiser poder fechá-lo por dentro"
Sephy

Respostas:

97

Assim como aquele belo aviso que você recebeu, você está tentando fazer algo que é um Antipadrão no React. Este é um não-não. O objetivo do React é que a desmontagem aconteça de um relacionamento de pai para filho. Agora, se você quiser que um filho se desmonte, pode simular isso com uma mudança de estado no pai que é acionada pelo filho. deixe-me mostrar em 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 é um exemplo muito simples. mas você pode ver uma maneira difícil de passar para os pais uma ação

Dito isso, você provavelmente deve passar pela loja (ação de despacho) para permitir que sua loja contenha os dados corretos quando for renderizada

Eu fiz mensagens de erro / status para dois aplicativos separados, ambos passaram pela loja. É o método preferido ... Se você quiser, posso postar um código de como fazer isso.

EDIT: Aqui está como eu configuro um sistema de notificação usando React / Redux / Typescript

Algumas coisas a serem observadas primeiro. isso está no texto datilografado, então você precisa remover as declarações de tipo :)

Estou usando os pacotes npm lodash para operações e nomes de classe (cx alias) para atribuição de nome de classe em linha.

A beleza dessa configuração é que eu uso um identificador exclusivo para cada notificação quando a ação a cria. (por exemplo, id_notificação). Este ID exclusivo é um Symbol(). Dessa forma, se você quiser remover qualquer notificação a qualquer momento, você pode porque sabe qual delas remover. Este sistema de notificação permitirá que você empilhe quantos quiser e eles irão embora quando a animação for concluída. Estou me conectando ao evento de animação e, quando termina, aciono um código para remover a notificação. Também configurei um tempo limite de fallback para remover a notificação, caso o callback da animação não seja acionado.

notificação-ações.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ção-redutor.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

na renderização de base para seu aplicativo, você renderizaria as notificações

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 notificação de usuário

/*
    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
fonte
1
"através da loja"? Acho que estou perdendo algumas lições cruciais sobre isso: D Obrigado pela resposta e pelo código, mas você não acha que isso é um exagero para um simples componente de exibição de mensagem de erro? Não deve ser responsabilidade dos pais lidar com uma ação definida sobre a criança ...
Sephy
Deve ser o pai na verdade, já que o pai é responsável por colocar o filho no DOM em primeiro lugar. Como eu estava dizendo, embora seja uma maneira de fazer isso, eu não recomendaria. Você deve estar usando uma ação que atualiza sua loja. ambos os padrões Flux e Redux devem ser usados ​​dessa maneira.
John Ruddell
Ok, então, ficarei feliz em obter alguns fragmentos de código, se você quiser. Voltarei a esse trecho de código quando tiver lido um pouco sobre Flux e Reduc!
Sephy
Ok, sim, acho que vou fazer um repositório github simples mostrando uma maneira de fazer isso. O último que fiz, usei animações css para fade in fade out o elemento que poderia renderizar elementos de string ou html e, em seguida, quando a animação foi concluída, usei javascript para ouvir isso e, em seguida, limpar-se (remover do DOM) quando o animação concluída ou você clicou no botão de dispensar.
John Ruddell
Por favor, faça isso, se isso puder ajudar outras pessoas como eu, que lutam um pouco para entender a filosofia do React. Além disso, eu ficaria feliz em compartilhar um pouco de meus pontos pelo tempo gasto se você colocar um repositório git para isso! Digamos cem pontos (recompensa disponível em 2 dias, no entanto)
Sephy
25

ao invés de usar

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

tente usar

ReactDOM.unmountComponentAtNode(document.getElementById('root'));
M Rezvani
fonte
Alguém já tentou fazer isso com React 15? Isso parece potencialmente útil e possivelmente um antipadrão.
theUtherSide
4
@ theUtherSide este é um anti-padrão na reação. Os documentos do React recomendam que você desmonte uma criança dos pais via state / adereços
John Ruddell
1
E se o componente que está sendo desmontado for a raiz do seu aplicativo React, mas não o elemento raiz que está sendo substituído? Por exemplo <div id="c1"><div id="c2"><div id="react-root" /></div></div>. E se o texto interno de c1for substituído?
flipdoubt
1
Isso é útil se você deseja desmontar seu componente raiz, especialmente se você tiver um aplicativo de reação residindo em um aplicativo sem reação. Tive que usar isso porque queria renderizar a reação dentro de um modal manipulado por outro aplicativo, e seu modal tem botões de fechamento que ocultarão o modal, mas meu reator ainda permanecerá montado. reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html
Abba
10

Na maioria dos casos, basta ocultar o elemento, por exemplo, desta forma:

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 você pode renderizar / renderizar / não renderizar via componente pai 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, existe uma maneira de remover o nó html, mas eu realmente não sei se é uma boa ideia. Talvez alguém que conhece React from internal diga algo sobre isso.

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
fonte
Mas, caso eu queira desmontar um filho que está dentro de uma lista de filhos ... O que posso fazer se quiser substituir um componente clonado pela mesma chave dessa lista?
roadev
1
pelo que entendi, você deseja fazer algo assim: document.getElementById (CHILD_NODE_ID) -> .remove (); -> document.getElementById (PARENT_NODE_ID) -> .appendChild (NEW_NODE)? Estou certo? Esqueça isso. NÃO é uma abordagem de reação. Usar estado de componente para renderização de condição
Sasha Kos,
2

Já estive neste post umas 10 vezes e só queria deixar meus dois centavos aqui. Você pode simplesmente desmontá-lo condicionalmente.

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

Tudo que você precisa fazer é removê-lo do DOM para desmontá-lo.

Contanto que renderMyComponent = trueo componente seja renderizado. Se você definir renderMyComponent = false, ele será desmontado do DOM.

ihodonald
fonte
0

Isso não é apropriado em todas as situações, mas você pode condicionalmente return falsedentro do próprio componente se um determinado critério for ou não atendido.

Não desmonta o componente, mas remove todo o conteúdo renderizado. Isso só seria ruim, em minha opinião, se você tiver ouvintes de eventos no componente que devem ser removidos quando o componente não for mais necessário.

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>
        );
    }
}
eco nebuloso
fonte