Jak odmontować, wycofać lub usunąć komponent z samego siebie w powiadomieniu React / Redux / Typescript

114

Wiem, że to pytanie zadawano już kilka razy, ale w większości przypadków rozwiązaniem jest radzenie sobie z tym u rodzica, ponieważ przepływ odpowiedzialności jest tylko malejący. Czasami jednak musisz zabić komponent jedną z jego metod. Wiem, że nie mogę modyfikować jego właściwości, a jeśli zacznę dodawać wartości logiczne jako stan, zacznie się robić naprawdę bałagan dla prostego komponentu. Oto, co próbuję osiągnąć: Mały komponent pola błędu z „x”, aby go odrzucić. Otrzymanie błędu za pośrednictwem jego właściwości spowoduje wyświetlenie go, ale chciałbym znaleźć sposób na zamknięcie go z własnego kodu.

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;

Użyłbym tego w następujący sposób w komponencie nadrzędnym:

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

W sekcji Co mam tu umieścić? , Już próbowałem:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode); Co rzuca niezły błąd w konsoli:

Ostrzeżenie: unmountComponentAtNode (): Węzeł, który próbujesz odmontować, został wyrenderowany przez React i nie jest kontenerem najwyższego poziomu. Zamiast tego poproś komponent nadrzędny o zaktualizowanie swojego stanu i ponowne wyrejestrowanie w celu usunięcia tego komponentu.

Czy powinienem skopiować przychodzące właściwości w stanie ErrorBox i manipulować nimi tylko wewnętrznie?

Sephy
źródło
Czy używasz Redux?
Arnau Lacambra
Dlaczego jest to wymóg „Otrzymanie błędu przez jego właściwości spowoduje wyświetlenie go, ale chciałbym znaleźć sposób na zamknięcie go z jego własnego kodu”? Normalnym podejściem byłoby wywołanie akcji, która usunęłaby stan błędu, a następnie została zamknięta w cyklu renderowania elementu nadrzędnego, jak wspomniałeś.
ken4z
W rzeczywistości chciałbym zaoferować taką możliwość. Rzeczywiście, będzie można go zamknąć, jak to wyjaśniłeś, ale mój przypadek jest taki, „co jeśli chcę mieć możliwość zamknięcia go od wewnątrz”
Sephy

Odpowiedzi:

97

Podobnie jak to miłe ostrzeżenie, które otrzymałeś, próbujesz zrobić coś, co jest anty-wzorcem w reakcji. To jest nie-nie. React ma na celu odłączenie od relacji rodzic-dziecko. Teraz, jeśli chcesz, aby dziecko się odmontowało, możesz to zasymulować za pomocą zmiany stanu w rodzicu, która jest wyzwalana przez dziecko. pokażę ci kod.

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

}

to jest bardzo prosty przykład. ale możesz zobaczyć szorstki sposób przekazania rodzicowi działania

Biorąc to pod uwagę, prawdopodobnie powinieneś przechodzić przez sklep (akcja wysyłkowa), aby Twój sklep zawierał prawidłowe dane, gdy ma być renderowany

Zrobiłem komunikaty o błędach / stanie dla dwóch oddzielnych aplikacji, obie przeszły przez sklep. Jest to preferowana metoda ... Jeśli chcesz, mogę opublikować kod, jak to zrobić.

EDYCJA: Oto jak skonfigurowałem system powiadomień za pomocą React / Redux / Typescript

Kilka rzeczy na początek. to jest w maszynie, więc musisz usunąć deklaracje typu :)

Używam pakietów npm lodash do operacji i nazw klas (alias cx) do przypisania w wierszu nazwy klasy.

Piękno tej konfiguracji polega na tym, że używam unikalnego identyfikatora dla każdego powiadomienia, gdy akcja je tworzy. (np. notify_id). Ten unikalny identyfikator to Symbol(). W ten sposób, jeśli chcesz usunąć jakiekolwiek powiadomienie w dowolnym momencie, możesz to zrobić, ponieważ wiesz, które z nich usunąć. Ten system powiadomień pozwoli Ci ułożyć tyle, ile chcesz, i znikną po zakończeniu animacji. Podłączam się do zdarzenia animacji i kiedy się kończy, uruchamiam kod, aby usunąć powiadomienie. Ustawiłem również rezerwowy limit czasu, aby usunąć powiadomienie na wypadek, gdyby wywołanie zwrotne animacji nie zostało uruchomione.

powiadomienia-działania.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);
    };
};

powiadomienie-reduktor.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

w podstawowym renderowaniu dla twojej aplikacji wyrenderowałbyś powiadomienia

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

klasa powiadamiania użytkownika

/*
    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
źródło
1
„przez sklep”? Myślę, że brakuje mi kilku kluczowych lekcji na ten temat: D Dziękuję za odpowiedź i kod, ale czy nie sądzisz, że to poważna przesada, jeśli chodzi o prosty element wyświetlania komunikatów o błędach? Rodzic nie powinien odpowiadać za działanie określone na dziecku ...
Sephy
Właściwie powinien to być rodzic, ponieważ to on jest odpowiedzialny za umieszczenie dziecka w DOM w pierwszej kolejności. Jak już mówiłem, chociaż jest to sposób na zrobienie tego, nie polecałbym tego. Powinieneś używać akcji, która aktualizuje Twój sklep. w ten sposób należy używać zarówno wzorców Flux, jak i Redux.
John Ruddell
W takim razie z przyjemnością zdobędę wskaźnik fragmentów kodu. Wrócę do tego fragmentu kodu, kiedy przeczytam trochę o Flux i Reduc!
Sephy
Ok, tak, myślę, że zrobię proste repozytorium github pokazujące, jak to zrobić. W ostatnim przypadku użyłem animacji css, aby zaniknąć element, który może renderować elementy ciągów lub html, a następnie, gdy animacja się zakończyła, użyłem javascript, aby nasłuchać, a następnie wyczyścić (usunąć z DOM), gdy albo animacja zakończona lub kliknąłeś przycisk odrzucenia.
John Ruddell
Proszę zrób to, jeśli to pomoże innym takim jak ja, którzy mają trudności ze zrozumieniem filozofii Reacta. Byłbym również szczęśliwy, mogąc rozstać się z odrobiną moich punktów za poświęcony czas, jeśli stworzysz w tym celu repozytorium git! Powiedzmy, że sto punktów (nagroda dostępna za 2 dni)
Sephy
25

zamiast używać

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

spróbuj użyć

ReactDOM.unmountComponentAtNode(document.getElementById('root'));
M Rezvani
źródło
Czy ktoś próbował tego z React 15? Wydaje się to zarówno potencjalnie przydatne, jak i prawdopodobnie anty-wzór.
theUtherSide
4
@theUtherSide to jest anty-wzór w reakcji. Dokumenty React zalecają odmontowanie dziecka od rodzica za pośrednictwem stanu / rekwizytów
John Ruddell
1
Co się stanie, jeśli odmontowywany komponent jest katalogiem głównym twojej aplikacji React, ale nie jest zastępowanym elementem głównym? Na przykład <div id="c1"><div id="c2"><div id="react-root" /></div></div>. Co się stanie, jeśli wewnętrzny tekst c1zostanie zastąpiony?
flipdoubt
1
Jest to przydatne, jeśli chcesz odmontować komponent główny, zwłaszcza jeśli masz aplikację reagującą znajdującą się w aplikacji niereagującej. Musiałem tego użyć, ponieważ chciałem renderować reakcję wewnątrz modalu obsługiwanego przez inną aplikację, a ich modalne mają zamknięte przyciski, które ukryją modal, ale moja reakcja pozostanie zamontowana. actjs.org/blog/2015/10/01/react-render-and-top-level-api.html
Abba
10

W większości przypadków wystarczy po prostu ukryć element, na przykład w ten sposób:

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

Lub możesz renderować / wyrenderować / nie renderować przez komponent nadrzędny w ten sposób

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

Wreszcie jest sposób na usunięcie węzła html, ale naprawdę nie wiem, czy to dobry pomysł. Może ktoś, kto zna Reacta od wewnątrz, powie coś na ten temat.

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
źródło
Ale w przypadku, gdy chcę odmontować dziecko znajdujące się na liście dzieci ... Co mogę zrobić, jeśli chcę zastąpić sklonowany komponent tym samym kluczem z tej listy?
roadev
1
jak rozumiem, chcesz zrobić coś takiego: document.getElementById (CHILD_NODE_ID) -> .remove (); -> document.getElementById (PARENT_NODE_ID) -> .appendChild (NEW_NODE)? Czy mam rację? Zapomnij o tym. To NIE jest podejście do reagowania. Użyj stanu komponentu do renderowania warunków
Sasha Kos,
2

Byłem w tym poście około 10 razy i chciałem tylko zostawić tutaj swoje dwa centy. Możesz po prostu odmontować go warunkowo.

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

Wszystko, co musisz zrobić, to usunąć go z DOM, aby go odmontować.

Tak długo, jak renderMyComponent = truekomponent będzie renderowany. Jeśli ustawisz renderMyComponent = false, odmontujesz go z DOM.

ihodonald
źródło
-1

Nie jest to właściwe we wszystkich sytuacjach, ale możesz warunkowo return falsewewnątrz samego komponentu, jeśli określone kryteria są lub nie są spełnione.

Nie odmontowuje komponentu, ale usuwa całą renderowaną zawartość. Moim zdaniem byłoby to złe tylko wtedy, gdyby w komponencie były detektory zdarzeń, które należy usunąć, gdy komponent nie jest już potrzebny.

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
źródło