React Warning: Can't call setState (or forceUp

2019-05-31 01:14发布

问题:

I have 2 components:
Orders - fetch some data and display it.
ErrorHandler - In case some error happen on the server, a modal will show and display a message.
The ErrorHandler component is warrping the order component

I'm using axios package to load the data in Orders component, and i use axios interceptors to setState about the error, and eject once the component unmounted.

When i navigate to the orders components back and forward i sometimes get an error in the console:

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
    in Orders (at ErrorHandler.jsx:40)
    in Auxiliary (at ErrorHandler.jsx:34)
    in _class2 (created by Route)

I tried to solve it by my previous case React Warning: Can only update a mounted or mounting component but here i can't make an axios token by the inspectors. Does anyone solve this issue before?

Here are my components:

Orders:

import React, { Component } from 'react';
import api from '../../api/api';
import Order from '../../components/Order/Order/Order';
import ErrorHandler from '../../hoc/ErrorHandler/ErrorHandler';

class Orders extends Component {
    state = {
        orders: [],
        loading: true
    }

    componentDidMount() {
        api.get('/orders.json')
            .then(response => {
                const fetchedOrders = [];
                if (response && response.data) {
                    for (let key in response.data) {
                        fetchedOrders.push({
                            id: key,
                            ...response.data[key]
                        });
                    }
                }
                this.setState({ loading: false, orders: fetchedOrders });
            })
            .catch(error => {
                this.setState({ loading: false });
            });
    }

    render() {
        return (
            <div>
                {this.state.orders.map(order => {
                    return (<Order
                        key={order.id}
                        ingrediencies={order.ingrediencies}
                        price={order.price} />);
                })}
            </div>
        );
    }
}

export default ErrorHandler(Orders, api);

ErrorHandler:

import React, { Component } from 'react';
import Auxiliary from '../Auxiliary/Auxiliary';
import Modal from '../../components/UI/Modal/Modal';

const ErrorHandler = (WrappedComponent, api) => {
    return class extends Component {
        requestInterceptors = null;
        responseInterceptors = null;
        state = {
            error: null
        };

        componentWillMount() {
            this.requestInterceptors = api.interceptors.request.use(request => {
                this.setState({ error: null });
                return request;
            });
            this.responseInterceptors = api.interceptors.response.use(response => response, error => {
                this.setState({ error: error });
            });
        }

        componentWillUnmount() {
            api.interceptors.request.eject(this.requestInterceptors);
            api.interceptors.response.eject(this.responseInterceptors);
        }

        errorConfirmedHandler = () => {
            this.setState({ error: null });
        }

        render() {
            return (
                <Auxiliary>
                    <Modal
                        show={this.state.error}
                        modalClosed={this.errorConfirmedHandler}>
                        {this.state.error ? this.state.error.message : null}
                    </Modal>
                    <WrappedComponent {...this.props} />
                </Auxiliary>
            );
        }
    };
};

export default ErrorHandler;

回答1:

I think that's due to asynchronous call which triggers the setState, it can happen even when the component isn't mounted. To prevent this from happening you can use some kind of flags :

  state = {
    isMounted: false
  }
  componentDidMount() {
      this.setState({isMounted: true})
  }
  componentWillUnmount(){
      this.state.isMounted = false
  }

And later wrap your setState calls with if:

if (this.state.isMounted) {
   this.setState({ loading: false, orders: fetchedOrders });
}


回答2:

You can't set state in componentWillMount method. Try to reconsider your application logic and move it into another lifecycle method.



回答3:

I think rootcause is the same as what I answered yesterday, you need to "cancel" the request on unmount, I do not see if you are doing it for the api.get() call in Orders component.

A note on the Error Handling, It looks overly complicated, I would definitely encourage looking at ErrorBoundaries provided by React. There is no need for you to have interceptors or a higher order component.

For ErrorBoundaries, React introduced a lifecycle method called: componentDidCatch. You can use it to simplify your ErrorHandler code to:

class ErrorHandler extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    this.setState({ hasError: true, errorMessage : error.message });
  }

  render() {
    if (this.state.hasError) {
      return <Modal 
                    modalClosed={() => console.log('What do you want user to do? Retry or go back? Use appropriate method logic as per your need.')}>
                    {this.state.errorMessage ? this.state.errorMessage : null}
                </Modal>
    }
    return this.props.children;
  }
}

Then in your Orders Component:

class Orders extends Component {
    let cancel;
    state = {
        orders: [],
        loading: true
    }

    componentDidMount() {
        this.asyncRequest = api.get('/orders.json', {
        cancelToken: new CancelToken(function executor(c) {
            // An executor function receives a cancel function as a parameter
            cancel = c;
            })
        })
            .then(response => {
                const fetchedOrders = [];
                if (response && response.data) {
                    for (let key in response.data) {
                        fetchedOrders.push({
                            id: key,
                            ...response.data[key]
                        });
                    }
                }
                this.setState({ loading: false, orders: fetchedOrders });
            })
            .catch(error => {
                this.setState({ loading: false });
                // please check the syntax, I don't remember if it is throw or throw new
                throw error;
            });
    }

    componentWillUnmount() {
       if (this.asyncRequest) {
          cancel();
       }
    }

    render() {
        return (
            <div>
                {this.state.orders.map(order => {
                    return (<Order
                        key={order.id}
                        ingrediencies={order.ingrediencies}
                        price={order.price} />);
                })}
            </div>
        );
    }
}

And use it in your code as:

<ErrorHandler>
   <Orders />
</ErrorHandler>