Как размонтировать, отрендерить или удалить компонент из самого себя в уведомлении React / Redux / Typescript

114

Я знаю, что этот вопрос уже задавали пару раз, но в большинстве случаев решение состоит в том, чтобы справиться с этим в родительском элементе, поскольку поток ответственности только нисходящий. Однако иногда вам нужно убить компонент одним из его методов. Я знаю, что не могу изменить его свойства, и если я начну добавлять логические значения в качестве состояния, для простого компонента это станет действительно беспорядочным. Вот чего я пытаюсь добиться: небольшой компонент окна ошибки с символом «x», чтобы его отклонить. Получение ошибки через его свойства отобразит ее, но я бы хотел закрыть ее из собственного кода.

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;

И я бы использовал это в родительском компоненте так:

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

В разделе Что здесь поставить? , Я уже пробовал:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode); Что вызывает красивую ошибку в консоли:

Предупреждение: unmountComponentAtNode (): узел, который вы пытаетесь размонтировать, был отрисован React и не является контейнером верхнего уровня. Вместо этого попросите родительский компонент обновить свое состояние и выполнить повторную визуализацию, чтобы удалить этот компонент.

Следует ли мне копировать входящие реквизиты в состояние ErrorBox и манипулировать им только внутренне?

Сефи
источник
Вы используете Redux?
Арнау Лакамбра
Почему это требование: «Получение ошибки через свойства будет отображать ее, но я хотел бы иметь способ закрыть ее из собственного кода»? Обычный подход - отправить действие, которое очистит состояние ошибки, а затем закроется в цикле рендеринга родительского объекта, как вы упомянули.
ken4z
На самом деле я хотел бы предложить возможность для обоих. Действительно, как вы объяснили, его можно будет закрыть, но в моем случае «что, если я также хочу иметь возможность закрыть его изнутри»
Сефи

Ответы:

97

Как и в том приятном предупреждении, которое вы получили, вы пытаетесь сделать что-то, что является антипаттерном в React. Это нет-нет. React предназначен для размонтирования родительских и дочерних отношений. Теперь, если вы хотите, чтобы дочерний элемент отключился, вы можете смоделировать это с помощью изменения состояния в родительском элементе, которое запускается дочерним элементом. позвольте мне показать вам код.

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

}

это очень простой пример. но вы можете увидеть грубый способ передать родителю действие

При этом вам, вероятно, следует пройти через магазин (действие отправки), чтобы ваш магазин содержал правильные данные, когда он идет на рендеринг.

Я делал сообщения об ошибках / статусе для двух отдельных приложений, оба прошли через магазин. Это предпочтительный метод ... Если хотите, я могу опубликовать код, как это сделать.

РЕДАКТИРОВАТЬ: вот как я настроил систему уведомлений с помощью React / Redux / Typescript

Прежде всего, следует отметить несколько моментов. это в машинописном тексте, поэтому вам нужно удалить объявления типов :)

Я использую lodash пакетов npm для операций и имена классов (псевдоним cx) для встроенного назначения имен классов.

Прелесть этой настройки в том, что я использую уникальный идентификатор для каждого уведомления, когда действие его создает. (например, notify_id). Этот уникальный идентификатор - это Symbol(). Таким образом, если вы хотите удалить любое уведомление в любой момент времени, вы можете это сделать, потому что вы знаете, какое из них нужно удалить. Эта система уведомлений позволит вам складывать столько, сколько вы хотите, и они исчезнут, когда анимация будет завершена. Я подключаюсь к событию анимации, и когда оно завершается, я запускаю код для удаления уведомления. Я также установил запасной тайм-аут, чтобы удалить уведомление на случай, если обратный вызов анимации не сработает.

уведомление-действия.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);
    };
};

уведомление-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

в базовом рендере для вашего приложения вы будете рендерить уведомления

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

класс уведомления пользователя

/*
    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
"через магазин"? Я думаю, мне не хватает нескольких важных уроков по этому поводу: D Спасибо за ответ и код, но не думаете ли вы, что это серьезный перебор для простого компонента отображения сообщений об ошибках? Родитель не должен отвечать за выполнение действия, определенного в отношении ребенка ...
Сефи
На самом деле это должен быть родитель, поскольку родитель отвечает за размещение дочернего элемента в DOM в первую очередь. Как я уже говорил, хотя это способ сделать это, я бы не рекомендовал его. Вы должны использовать действие, обновляющее ваш магазин. Таким образом следует использовать как шаблоны Flux, так и шаблоны Redux.
Джон Радделл
Хорошо, я был бы счастлив получить указатель на фрагменты кода. Я вернусь к этому фрагменту кода, когда узнаю немного о Flux и Reduc!
Сефи
Хорошо, да, я думаю, что сделаю простой репозиторий на github, показывающий, как это сделать. В последнем, что я сделал, я использовал css-анимацию для постепенного исчезновения элемента, который может отображать строковые или html-элементы, а затем, когда анимация завершилась, я использовал javascript, чтобы прослушать это, а затем очистить себя (удалить из DOM), когда либо анимация завершена, или вы нажали кнопку закрытия.
Джон Радделл
Пожалуйста, сделайте это, если это может помочь другим вроде меня, которые немного не могут понять философию React. Кроме того, я бы с радостью поделился своими впечатлениями о потраченном времени. Если вы все же разместите репозиторий git для этого! Допустим, сто очков (награда доступна через 2 дня)
Сефи
25

Вместо того, чтобы использовать

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

попробуйте использовать

ReactDOM.unmountComponentAtNode(document.getElementById('root'));
М Резвани
источник
Кто-нибудь пробовал это с React 15? Это кажется одновременно потенциально полезным и, возможно, антипаттерном.
theUtherSide
4
@theUtherSide это антипаттерн в React. Документы React рекомендуют размонтировать дочерний элемент от родителя с помощью state / props
Джон Радделл,
1
Что делать, если отключаемый компонент является корнем вашего приложения React, но не заменяемым корневым элементом? Например <div id="c1"><div id="c2"><div id="react-root" /></div></div>. Что делать, если внутренний текст c1заменяется?
flipdoubt 03
1
Это полезно, если вы хотите отключить корневой компонент, особенно если у вас есть реагирующее приложение, находящееся в нереагирующем приложении. Мне пришлось использовать это, потому что я хотел отобразить реакцию внутри модального окна, обрабатываемого другим приложением, и в их модальном окне есть кнопки закрытия, которые скроют модальное окно, но моя реакция все равно останется смонтированной. reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html
Abba
10

В большинстве случаев достаточно просто скрыть элемент, например так:

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

Или вы можете отображать / повторно отображать / не отображать через родительский компонент, например

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

Наконец, есть способ удалить узел html, но я действительно не знаю, хорошая ли это идея. Может быть, кто-нибудь, кто знает React изнутри, что-нибудь скажет по этому поводу.

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>
        );
    }
}
Саша Кос
источник
Но в случае, если я хочу размонтировать дочерний элемент, который находится внутри списка дочерних элементов ... Что мне делать, если я хочу заменить клонированный компонент с тем же ключом в этом списке?
roadev
1
насколько я понимаю, вы хотите сделать что-то вроде этого: document.getElementById (CHILD_NODE_ID) -> .remove (); -> document.getElementById (PARENT_NODE_ID) -> .appendChild (NEW_NODE)? Я прав? Забудь об этом. Это НЕ реактивный подход. Использовать состояние компонента для рендеринга условий
Саша Кос
2

Я был на этом посте около 10 раз и просто хотел оставить здесь свои два цента. Можно просто размонтировать условно.

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

Все, что вам нужно сделать, это удалить его из DOM, чтобы размонтировать.

Пока renderMyComponent = trueкомпонент будет отображаться. Если вы установите renderMyComponent = false, он отключится от DOM.

иходональд
источник
-1

Это не подходит для всех ситуаций, но вы можете условно return falseвнутри самого компонента, если определенные критерии соблюдены или не выполнены.

Он не отключает компонент, но удаляет весь отображаемый контент. На мой взгляд, это было бы плохо, только если у вас есть прослушиватели событий в компоненте, которые следует удалить, когда компонент больше не нужен.

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>
        );
    }
}
туманность
источник