Search code examples
javascriptreactjsweb-component

React Can't perform a React state update on an unmounted component error


I made a login form so when someone logs in successfully, the server sends the user info (name, email, id) and the some page states change accordingly (like route, user object), but whenever I log in everything works well but i get an error in the console saying:

index.js:1 Warning: Can't perform a React state update 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 Login (at App.js:226)

It really confuses me bcs the app still works well and idk why i'm getting this error.

app.js's render():

  render() {

    if (this.state.route === 'login') {
      return <Login loadUser={this.loadUser} routeChange={this.routeChange} /> //this is line 226 at app.js

    } else if (this.state.route === 'register') {
      return <Register loadUser={this.loadUser} routeChange={this.routeChange} />

    } else {
      return (
        <div>
          {/* The rest of the app's home components */}
        </div>
      )
    }

login.js:

    class Login extends Component {
    constructor(props) {
        super(props);
        this.state = {
            email: "",
            password: "",
            loading: false
        }
    }

    handleChange = (e) => {
        this.setState({ [e.target.name]: e.target.value })

    }

    handleClick = () => {

        this.setState({ loading: true })
        fetch('myApi/login', {
            method: 'post',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                email: this.state.email,
                password: this.state.password
            })
        })
            .then(response => response.json())
            .then(user => {
                if (user.id) {
                    this.props.routeChange('home');

                    this.props.loadUser(user);
                    //localStorage.setItem('user', JSON.stringify(user));
                    this.setState({ loading: false })
                }
            })
    }

    render() {
        return (
            <div className="container">
                <div className="row">
                    <div className="col-sm-9 col-md-7 col-lg-5 mx-auto">
                        <div className="card card-signin my-5">
                            <div className="card-body">
                                <h5 className="card-title text-center">Sign In</h5>
                                <form onSubmit={(e) => e.preventDefault()} className="form-signin">
                                    <div className="form-label-group">
                                        <input onChange={this.handleChange} name="email" type="email" id="inputEmail" className="form-control" placeholder="Email address" autoFocus />
                                        <label htmlFor="inputEmail">Email address</label>
                                    </div>
                                    <div className="form-label-group">
                                        <input onChange={this.handleChange} name="password" type="password" id="inputPassword" className="form-control" placeholder="Password" />
                                        <label htmlFor="inputPassword">Password</label>
                                    </div>
                                    <div className="custom-control custom-checkbox mb-3">
                                        <input type="checkbox" className="custom-control-input" id="customCheck1" />
                                        <label className="custom-control-label" htmlFor="customCheck1">Remember password</label>
                                    </div>
                                    <button onClick={this.handleClick} className="btn btn-lg btn-primary btn-block text-uppercase">Sign in</button>
                                    <hr className="my-4" />
                                    <h6 onClick={() => this.props.routeChange('register')} style={{ textAlign: 'center', cursor: 'pointer' }}>Register instead</h6>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

Solution

  • The issue is this thenable condition, when you setState after the route changed. Remember state updates are asynchronous and occur before each new render cycle.

    if (user.id) {
      this.props.routeChange('home');
      this.props.loadUser(user);
      //localStorage.setItem('user', JSON.stringify(user));
      this.setState({ loading: false }) // <-- error since component unmounted!!
    }
    

    Removing the loading state back to false should address warning:

    if (user.id) {
      this.props.routeChange('home');
      this.props.loadUser(user);
    }
    

    EDIT To really fix this correctly, move the route change to a lifecycle function so you can correctly set loading state back to false if login didn't work or user object doesn't have id property.

    handleClick = () => {
      this.setState({ loading: true })
      fetch('myApi/login', {
        method: 'post',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email: this.state.email,
          password: this.state.password
        })
      })
        .then(response => response.json())
        .then(user => {
          if (user.id) {
            this.props.loadUser(user);
            this.setState({ user })
          }
        })
        .finally(() => this.setState({ loading: false })); // run this no matter what
    }
    
    componentDidUpdate(prevProps, prevState) {
      // check user state condition, if met, change route
      if (/* user condition met */) {
        this.props.routeChange('home');
      }
    }