如何在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吗?
Arnau Lacambra '16

为什么有这个要求“通过道具接收错误会显示出来,但是我想从自己的代码中关闭它”。通常的方法是调度一个操作,该操作将清除错误状态,然后在您提到的父级的渲染周期中将其关闭。
ken4z

我实际上想为两者提供可能性。确实,它将如您所解释的那样关闭,但是我的情况是“如果我也想能够从内部关闭它,该
怎么办

Answers:


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设置通知系统的方法

首先要注意的事情。这是在打字稿中,所以您需要删除类型声明:)

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

此设置的好处是,在操作创建通知时,我为每个通知使用唯一的标识符。(例如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);
    };
};

通知减少器

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

用户通知

用户通知类

/*
    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感谢您的回答和代码,但您不认为这对于简单的错误消息显示组件来说严重过头了吗?家长不应负责处理对孩子定义的诉讼……
Sephy

实际上,它应该是父母,因为父母首先负责将孩子放在DOM中。就像我刚才说的那样,即使这是一种实现方式,我也不建议这样做。您应该使用更新商店的操作。Flux和Redux模式都应以这种方式使用。
约翰·鲁德尔

好吧,如果您愿意,我很高兴获得一些代码片段的指针。当我阅读了有关Flux和Reduc的文章时,我会回到那段代码!
塞菲

好吧,是的,我想我将做一个简单的github回购,展示一种实现方法。我做的最后一个操作是使用css动画淡入淡出可以渲染字符串或html元素的元素,然后当动画完成时,我使用javascript侦听了该内容,然后清理了自身(从DOM中删除),动画完成或您点击了关闭按钮。
约翰·鲁德尔

请这样做,如果它可以帮助像我这样努力奋斗以掌握React理念的其他人。另外,如果您为此设置了一个git repo,我很乐意分享我的观点。可以说一百分(尽管两天内可以提供赏金)
Sephy '16

25

而不是使用

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

尝试使用

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

有人在React 15上尝试过吗?这似乎既有用,又可能是反模式。
theUtherSide

4
@theUtherSide这是反应中的反模式。React docs建议您通过状态/道具从父母那里卸下孩子
John Ruddell,

1
如果要卸载的组件是您的React应用程序的根目录而不是被替换的根元素怎么办?例如<div id="c1"><div id="c2"><div id="react-root" /></div></div>。如果c1替换了的内部文本怎么办?
flipdoubt

1
如果要卸载根组件,这特别有用,尤其是当您有一个React应用驻留在非反应应用中时。我必须使用它,因为我想在另一个应用程序处理的模态中渲染反应,并且它们的模态具有关闭按钮,这些按钮将隐藏模态,但我的反应性仍然保持安装状态。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)?我对吗?忘掉它。这不是反应方法。使用组件状态进行状态渲染
Sasha Kos

2

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

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

您要做的就是将其从DOM中删除以进行卸载。

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


0

这并非在所有情况下都适用,但是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>
        );
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.