如何在 React/Redux/Typescript 通知消息中从自身卸载、取消呈现或删除组件

How to unmount, unrender or remove a component, from itself in a React/Redux/Typescript notification message

我知道这个问题已经被问过几次了,但大多数时候,解决方案是在父级中处理这个问题,因为责任的流动只会递减。但是,有时,您需要通过其中一种方法终止组件。 我知道我不能修改它的道具,如果我开始添加布尔值作为状态,对于一个简单的组件来说它会开始变得非常混乱。这是我想要实现的目标: 一个小的错误框组件,带有一个“x”来关闭它。通过它的 props 接收错误将显示它,但我想要一种从它自己的代码中关闭它的方法。

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); 这在控制台中引发了一个很好的错误:

Warning: unmountComponentAtNode(): The node you're attempting to unmount was rendered by React and is not a top-level container. Instead, have the parent component update its state and rerender in order to remove this component.

我是否应该在 ErrorBox 状态下复制传入的 props,并仅在内部对其进行操作?

就像你收到的那个好警告一样,你正在尝试做一些在 React 中是 Anti-Pattern 的事情。这是一个no-no。 React 旨在从 parent 到 child 关系中进行卸载。现在,如果您希望 child 卸载自身,您可以通过 child 触发的 parent 中的状态更改来模拟此操作。让我用代码告诉你。

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

}

这是一个非常简单的例子。但是你可以看到一个粗略的方法来传递给parent一个动作

话虽如此,您可能应该遍历商店(调度操作)以允许您的商店在呈现时包含正确的数据

我已经为两个单独的应用程序完成了 error/status 消息,都通过了商店。这是首选方法...如果您愿意,我可以 post 一些关于如何做到这一点的代码。

编辑:以下是我如何使用 React/Redux/Typescript

设置通知系统

首先要注意几点。这是打字稿,所以你需要删除类型声明:)

我正在使用 npm 包 lodash 进行操作,并使用 classnames(cx 别名)进行内联 classname 分配。

此设置的美妙之处在于我在操作创建通知时为每个通知使用唯一标识符。 (例如 notify_id)。这个唯一 ID 是 Symbol()。这样,如果您想在任何时间点删除任何通知,您都可以,因为您知道要删除哪个通知。这个通知系统可以让你随心所欲地堆叠,当动画完成时它们就会消失。我挂接到动画事件,当它完成时,我触发一些代码来删除通知。我还设置了一个回退超时来删除通知,以防动画回调没有触发。

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

在您的应用程序的基础呈现中,您将呈现通知

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

用户通知class

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

而不是使用

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

尝试使用

ReactDOM.unmountComponentAtNode(document.getElementById('root'));

大多数情况下,隐藏元素即可,例如:

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

或者您可以render/rerender/not像这样通过父组件呈现

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

我已经去过这个 post 大约 10 次了,我只想把我的两分钱留在这里。您可以有条件地卸载它。

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

您只需将其从 DOM 中删除即可卸载。

只要renderMyComponent = true,组件就会渲染。如果您设置 renderMyComponent = false,它将从 DOM.

卸载

这并不适用于所有情况,但如果满足或不满足特定条件,您可以有条件地 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>
        );
    }
}