Replacing componentWillMount() with constructor does not work

Viewed 505

A class is used to intercept axios error. to bind arrow functions, componentDidMount() is being used. now I need to initiliaze data from the server so I have to use componentWillMount() except it will be removed in React 17 and warning message suggest I use constructor. When I do it gives me an error.

import React, {Component} from "react";

import Modal from "../../components/UI/Modal/Modal";
import Aux from "../Auxiliary/Auxiliary";

const withErrorHandler = (WrappedComponent, axios) => {
    return class extends Component{
        state = {
            error: null
        };

        // componentWillMount() {
        //  axios.interceptors.request.use(request => {
        //      this.setState({
        //          error: null
        //      });
        //      return request;
        //  });
        //  axios.interceptors.response.use(res => res, (error) => {
        //      this.setState({
        //          error: error
        //      })
        //  });
        // }

        constructor(props) {
            super(props);
            axios.interceptors.request.use(request => {
                this.setState({
                    error: null
                });
                return request;
            });
            axios.interceptors.response.use(res => res, (error) => {
                this.setState({
                    error: error
                })
            });
        }



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

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

export default withErrorHandler;

I removed .json from URL to produce an error

class BurgerBuilder extends Component {
    state = {
        ingredients: null,
        totalPrice: 4,
        purchasable: false,
        purchasing: false,
        loading: false,
        // axiosError: null
    };

    componentDidMount() {
        axios.get('https://burger-m.firebaseio.com/ingredients').then(response => {
            this.setState({ingredients: response.data});
        }).catch(error => {});
    }
..
export default withErrorHandler(BurgerBuilder, axios);

&

Error Message: "index.js:1 Warning: Can't call setState on a component that
 is not yet mounted. This is a no-op, but it might indicate a bug in your 
application. Instead, assign to `this.state` directly or define a `state = {};` 
class property with the desired state in the _temp component."

componentWillMount() does work however. so What Should I change?

5 Answers

Keep constructor simple by just adding state and do not register axios interceptors in constructor method, instead register interceptors in render method.

 componentWillUnmount(){
        console.log('unregistering interceptors', this.reqInterceptor, this.resInterceptor)
        axios.interceptors.request.eject(this.reqInterceptor);
        axios.interceptors.response.eject(this.resInterceptor);
    }

render() {
        if(!this.resInterceptor){
            console.log('Registering Interceptors');
            this.reqInterceptor = axios.interceptors.request.use(req => {
                this.setState({ error: null })
                return req;
            })
            this.resInterceptor = axios.interceptors.response.use(response => response, error => {
                this.setState({error})
            })
        }

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

The constructor initializes the state, that's why you are prohibited from using setState() there.

You could use componentDidMount() instead, I think it matches better your needs and will avoid any confusion.

const withErrorHandler = (WrappedComponent, axios) => {
    return class extends Component{
        state = {
            error: null
        };

        componentDidMount() {
            axios.interceptors.request.use(request => {
                this.setState({
                    error: null
                });
                return request;
            });

            axios.interceptors.response.use(res => res, (error) => {
                this.setState({
                    error: error
                })
            });
        }

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

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

Use this.state = {error: null} and this.state = {error} instead of setState in then blocks of interceptors.

Regarding this specific example I think the best solution is to use a "temporary" variable , call the use method on the interceptors, and the define the state. All this inside the constructor. Like so:

constructor() {
        super();
        let tempErrorState = null;

        this.requestInterceptor = axios.interceptors.request.use(req => {
            tempErrorState = null;
            return req;
        });
        this.resoponseInterceptor = axios.interceptors.response.use(res => res, error => {
            tempErrorState = error;
        });
        this.state = {
            error: tempErrorState
        };
    }

As per your condition, you can't use the componentDidMount() method. You MUST NOT use the render() method for setting up Axios interceptors because you are using withErrorHandler as a higher-order component which can lead to many interceptors eventually created while wrapping other components too. This is because for each render cycle, you are unnecessarily setting up an interceptor if you define it inside a render() method. You should set up an Axios interceptor only once in the component.

The constructor() would be the best place to set this up (as per the latest version of React where componentWillMount() is deprecated).

You can edit your code to include a constructor():

constructor(props) {
    super(props);

    this.state = {
        error: null
    };

    // clear the error when sending a request
    axios.interceptors.request.use(req => {
        this.state = {
            error: null
        };
        return req;
    });

    axios.interceptors.response.use(res => res, err => {
        this.state = {
            error: err
        };
    });
}

Notice that here, you are not using the setState() method which throws a warning in constructor but you are directly setting the state, which is allowed only inside the constructor().

Related