如何在 React/Redux/Typescript 通知消息中从自身卸载、取消渲染或移除组件

IT技术 reactjs unmount
2021-04-15 04:40:47

我知道这个问题已经被问过几次了,但大多数时候,解决方案是在父级中处理这个问题,因为责任流只会下降。但是,有时,您需要从其中一种方法中杀死一个组件。我知道我不能修改它的 props,如果我开始添加布尔值作为状态,对于一个简单的组件来说,它会开始变得非常混乱。这是我想要实现的目标:一个小的错误框组件,用“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); 这在控制台中引发了一个很好的错误:

警告:unmountComponentAtNode():您尝试卸载的节点是由 React 渲染的,不是顶级容器。相反,让父组件更新其状态并重新渲染以删除此组件。

我应该在 ErrorBox 状态下复制传入的props,并仅在内部操作它吗?

5个回答

就像你得到的那个很好的警告一样,你正在尝试做一些 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 设置通知系统

首先要注意几点。这是在typescript中,因此您需要删除类型声明:)

我使用 npm 包 lodash 进行操作,使用类名(cx 别名)进行内联类名分配。

这个设置的美妙之处在于,当操作创建它时,我为每个通知使用一个唯一的标识符。(例如notify_id)。此唯一 ID 是一个Symbol(). 这样,如果您想在任何时间点删除任何通知,您都可以,因为您知道要删除哪个通知。此通知系统可让您根据需要堆叠任意数量,并且在动画完成后它们将消失。我正在连接动画事件,当它完成时,我触发一些代码来删除通知。我还设置了回退超时以删除通知,以防动画回调未触发。

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

通知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;
};

应用程序.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>
    );
}

用户通知.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>
        );
    }
}
好的,如果您愿意,我很乐意得到一些代码片段的指针。当我阅读了一些关于 Flux 和 Reduc 的内容后,我会回到那段代码!
2021-05-22 04:40:47
它实际上应该是父级,因为父级首先负责将子级放入 DOM 中。就像我说的那样,即使这是一种方法,我也不会推荐它。您应该使用更新商店的操作。Flux 和 Redux 模式都应该这样使用。
2021-06-03 04:40:47
好的,是的,我想我会制作一个简单的 github repo 来展示一种方法。我做的最后一个我使用 css 动画淡入淡出可以呈现字符串或 html 元素的元素,然后当动画完成时我使用 javascript 来监听它然后清理自己(从 DOM 中删除)当动画完成或您单击了关闭按钮。
2021-06-12 04:40:47
请这样做,如果它可以帮助像我这样在理解 React 哲学方面有些困难的人。另外,如果你为此提供了一个 git repo,我很乐意在所花费的时间里分享一些我的观点!让我们说一百分(尽管在 2 天内可以获得赏金)
2021-06-13 04:40:47
“通过商店”?我想,我错过了一些重要的教训:D 感谢您的回答和代码,但您不认为这对于一个简单的错误消息显示组件来说是严重的矫枉过正吗?处理对孩子定义的操作不应该是父母的责任......
2021-06-21 04:40:47

而不是使用

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

尝试使用

ReactDOM.unmountComponentAtNode(document.getElementById('root'));
如果被卸载的组件是你的 React 应用程序的根而不是被替换的根元素怎么办?例如<div id="c1"><div id="c2"><div id="react-root" /></div></div>如果内部文本c1被替换怎么办?
2021-05-23 04:40:47
具有讽刺意味的是,有一天宇宙中的所有其他事物都发现自己是 React 的“反模式”,而 React 开发本身正在成为一种反模式。当您的代码的 80% 是流程/样板以保护其他 20% 免受未来开发人员的影响时,就会出现问题。它是我最喜欢的开发 Web 应用程序的框架,但它只是一个工具,而不是正统。
2021-05-26 04:40:47
有人用 React 15 试过这个吗?这似乎既潜在有用又可能是一种反模式。
2021-05-31 04:40:47
@theUtherSide 这是反应中的反模式。React 文档建议您通过 state / props 从父级卸载子级
2021-06-06 04:40:47
如果您想卸载根组件,这将非常有用,尤其是当您的 React 应用程序驻留在非 React 应用程序中时。我不得不使用它,因为我想在另一个应用程序处理的模态内呈现反应,并且它们的模态有关闭按钮,可以隐藏模态,但我的 reactdom 仍将保持安装状态。reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html
2021-06-18 04:40:47

在大多数情况下,仅隐藏元素就足够了,例如以这种方式:

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>
        );
    }
}
据我所知,你想做这样的事情: document.getElementById( CHILD_NODE_ID ) -> .remove(); -> document.getElementById(PARENT_NODE_ID) -> .appendChild(NEW_NODE)?我对吗?忘掉它。这不是反应方法。使用组件状态进行条件渲染
2021-05-30 04:40:47
但是,如果我想卸载子列表中的子项...如果我想用该列表中的相同键替换克隆组件,我该怎么办?
2021-06-11 04:40:47

我已经去过这个帖子大约 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>
        );
    }
}