【问题标题】:Unmounted setState warning even though the component is mounted即使组件已安装,也未安装 setState 警告
【发布时间】:2019-09-04 10:43:11
【问题描述】:

在我的 ReactJS@15 项目中,我遇到了 unmount setState change warning 的问题,但组件已安装。

这是应用程序结构:

/app
  /container
     /veil
     /router
        /routed-view
        /other-routed-view

我还有一个“veil manager class”,它以这种方式触发附加到veil组件的“_toggle”事件:

componentDidMount()
{
    VeilManager.i().on('toggle', this._toggle.bind(this));
}

_toggle(payload = {})
{
    const { veil = false, cb } = payload;
    this.setState({ isOpen: veil }, cb);
}

从路由视图我触发以下代码:

componentDidMount()
{
   VeilManager.i().toggle(this._otherFunc.bind(this));
}

在调试流程中,Veil 组件在触发事件时被标记为已卸载,但完全没有意义,因为容器已经注册到其子级。

更重要的是,Veil 组件会按预期做出反应,因此当 VeilManager 状态更改时,Veil 会切换进出。

有什么建议吗?

扩展代码

// Veil.js
import { background, logo, veil }   from './Styles';
import React, { Component }         from 'react';
import VeilManager                  from './Manager';


export default class Veil extends Component
{
    constructor(props)
    {
        super(props);

        this.state = {
            isOpen: false
        };
    }

    componentDidMount()
    {
        VeilManager.i().on('toggle', this._toggle.bind(this));
    }

    _toggle(payload = {})
    {
        const { veil = false, cb } = payload;
        this.setState({ isOpen: veil }, cb);
    }

    /**
     * @override
     */
    render()
    {
        const { isOpen = false } = this.state;

        return (
            <div style={veil(isOpen)} className="veil-wrapper">
                <div style={logo} className="veil-wrapper__logo"/>
                <div style={background}/>
            </div>
        );
    }
}
// VeilManager.js
const { EventEmitter } = require('events');

/**
 * VeilManager singleton reference
 * @type {null}
 */
let $iManager = null;

/**
 * Handles the status of veil
 * @class   veil.Manager
 * @extends EventEmitter
 */
export default class Manager extends EventEmitter
{
    /**
     * @constructor
     */
    constructor()
    {
        super();
        this.isOpen = false;
    }

    /**
     * Toggles "isOpen" status
     * @param   {null|Function} cb  To execute after toggling
     */
    toggle(cb = null)
    {
        this.isOpen = !this.isOpen;
        this.emit('toggle', { veil: this.isOpen, cb });
    }

    /**
     * Returns the singleton instance of VeilManager
     * @return {null|Manager}   Singleton instance
     */
    static i()
    {
        $iManager = $iManager || new Manager();

        return $iManager;
    }
}
//Container.js
import 'react-s-alert/dist/s-alert-css-effects/slide.css';
import 'react-s-alert/dist/s-alert-default.css';
import { Redirect, Route, Switch }  from 'react-router-dom';
import React, { Component }         from 'react';
import Alert                        from 'react-s-alert';
import Aside                        from '@/components/Aside';
import Header                       from '@/components/Header';
import token                        from '@/orm/Token';
import Sidebar                      from '@/components/Sidebar';
import Veil                         from '@/components/Veil';

// VIEWS
import Clients              from '../../views/Clients/';
import Dashboard            from '@/views/Dashboard';

export default class Full extends Component
{
    /**
     * @override
     */
    render()
    {
        return (
            <div className="app">
                <Header />
                <div className="app-body">
                    <Sidebar {...this.props}/>
                    <main className="main">
                        <Veil/>
                        <div className="container-fluid">
                            { token.hasExpired()
                                ? <Redirect to="/login"/>
                                : <Switch>
                                    <Route path="/dashboard" name="Dashboard" component={Dashboard}/>
                                    <Route path="/clients" name="Clients" component={Clients}/>
                                    <Redirect to="/dashboard"/>
                                </Switch>
                            }
                        </div>
                    </main>
                    <Aside />
                </div>
                <Alert stack={{ limit : 3 }} />
            </div>
        );
    }
}
//Clients.js
import React, { Component } from 'react';
import onFetch              from '@/mixins/on-fetch';

/**
 * Clients view.
 */
export default class ClientsIndex extends Component
{
    /**
     * @constructor
     */
    constructor(props)
    {
        super(props);

        this._onFetch = onFetch;
    }

    /**
     * @override
     */
    componentDidMount()
    {
        this._onFetch();
    }

    /**
     * @override
     */
    render()
    {
        return (
            <div className="animated fadeIn">
                <div className="row">
                  ...
                </div>
            </div>
        );
    }
}
//mixin/on-fetch
import VeilManager from '@/components/Veil/Manager';

export default (cb = null) => {
    debugger;
    VeilManager.i().toggle(cb);
};

【问题讨论】:

  • 更新可能会冒泡并导致高位组件刷新,这意味着 setstate 现在作为之前未安装的组件无效
  • 这看起来像是冒泡的情况,你能提供更多代码吗?
  • 但是 Veil 组件是独立的,容器组件不与其子组件或 Veil 本身共享其状态。孩子们也不会向上分享状态。
  • @DarpanRangari 添加了额外的代码
  • @ArturoMartínezDíaz 未声明,冒泡。 DOM 中的事件从子级向上冒泡到父级,React 不会改变这一点。如果您使用 CustomEvent 构造函数,也可以将其设置为 false。另外,你是如何在浏览器中使用 EventEmitter 的?

标签: javascript reactjs lifecycle


【解决方案1】:

解决方案

正如 cmets 中所指出的,是冒泡事件和状态传播的问题。

所做的更改:

// Veil manager
...
toggle(cb = null)
{
    this.isOpen = !this.isOpen;
    const detail = { veil: this.isOpen, cb };
    window.dispatchEvent(new CustomEvent('veil-toggle', { bubbles: false, detail }));
}
...
// Veil
...
/**
 * @override
 */
constructor(props)
{
    super(props);

    this.state = {
        open: false
    };
}

/* istanbul ignore next */
componentWillReceiveProps(next)
{
    const open = next && !!next.open;
    this.setState({ open });
}

/**
 * @override
 */
render()
{
    return (
        <div style={veil(this.state.open)} className="veil-wrapper">
            <div style={logo} className="veil-wrapper__logo"/>
            <div style={background}/>
        </div>
    );
}
...
// Container
...
componentDidMount()
{
    window.addEventListener('veil-toggle', this._toggleVeil.bind(this));
}

_toggleVeil(e)
{
   this.setState({ veil: e.detail.veil }, e.detail.cb);
}

render()
{
    ...
    <Veil open={this.state.veil}/>
    ...
}
...

【讨论】:

    【解决方案2】:

    根据上面的代码,如果veil组件卸载后veilManager触发'toggle'事件,那么你会遇到这个错误。

    你可以通过两种方式解决:

    1. 移除 componentWillUnmount 中的 'onToggle' 监听器,这样如果 Veil 组件被卸载,则无需运行其处理程序。

    2. 或者,您可以在 VeilComponent 中保留一个变量 this.mountedState 并创建一个单独的 setStateSafe 方法,该方法在调用 setState 之前检查mountedState。

        componentDidMount() {
            this.mountedState = true;
            VeilManager.i().on('toggle', this._toggle.bind(this));
        }
    
        _toggle(payload = {}) {
            const { veil = false, cb } = payload;
            this.setStateSafe({ isOpen: veil }, cb);
        }
    
        setStateSafe(nextState, cb) {
            if(this.stateMounted) {
                 this.setState(nextState, cb);
            }
        }
    

    我建议选项 1,因为我们不应该为已卸载的组件调用切换处理程序。谢谢

    【讨论】:

      猜你喜欢
      • 2018-01-29
      • 2021-09-27
      • 2017-04-26
      • 2016-10-01
      • 2016-02-22
      • 1970-01-01
      • 2016-10-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多