hillybob991
hillybob991

Reputation: 97

setState isn't updating state

I'm trying to render components in App.js when a user logs in but when I setState in App.js (in order to render components), the state does not change

Firstly, I send the username and password from login.js to App.js here:

  loginSubmit= (e) => {
    e.preventDefault()

      if(this.state.username === "") {
        this.showValidationErr("username", "Username cannot be empty")
      } if (this.state.password === "") {
        this.showValidationErr("password", "Password cannot be empty")
      }


    const username = this.state.username
    this.props.login(username)

This is rendered in App.js here:

render() {
    return (
      <div className="container">
        <h1>Lightning Talks!</h1>
          <Login login={this.login}/>
            {this.state.loggedIn ? <lightningTalkRender/> : null}
          <h3 className="form-header"> Submit your talk</h3>
        <Form postInApp={this.postInApp}/>
      </div>
    )
  }

It should call the login function in App.js (which is done because I console.log(username) and it is received):

 login = (username) => {
    console.log('username', username)
    console.log('username state', this.state.username)

    this.setState({
      loggedIn: true,
      username: username
    });
    console.log('username', username) // username typed in is logged
    console.log('username state', this.state.username) //username in state is empty

    console.log('logged in state', this.state.loggedIn) // logged-In is still false
  }

When this is done, logged-In should become true, which renders the lightningTalkComponent in App.js (see above also):

{this.state.loggedIn ? <lightningTalkRender/> : null}

The initial state in App.js is this:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      lightningTalks: [],
      username: "",
      loggedIn: false
    };
  }

Why isn't the state updating?

Full code is here:

import React from 'react';
import LightningTalk from './components/lightning-talk-component.js';
import Form from './components/form.js';
import Login from './components/login.js';
import './App.css'


// initialized state of App to hold an empty lightningTalks compoennt. componentDidMount sets its state depends
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      lightningTalks: [],
      username: "",
      loggedIn: false
    };
  }

// componentDidMount is called and sets the state of the lightningTalks array in constructor(props)
  componentDidMount = () => {
    fetch("http://localhost:4000/talks.json")
    .then(response => response.json())
    .then((data) => {
      // sorts the data when component mounts from largest to smallest votes
      data.sort((a, b) => b.votes - a.votes)
      this.setState((state) => {
        return {
          lightningTalks: data
        };
      });
    });
  }

// sends a post request to the API
  postInApp = (titleDescription) => {
    const talk = {}
    talk.title = titleDescription.title
    talk.description = titleDescription.description
    talk.votes = 0

      fetch("http://localhost:4000/talks", {
        headers: {
          "Content-Type": "application/json"
        },
        method: "POST",
        body: JSON.stringify({ "talk": talk })
        })
      .then(response => response.json())
      .then((data) => {
         console.log(data);
      });
  }

  login = (username) => {
    console.log('username', username)
    console.log('username state', this.state.username)

    this.setState({
      loggedIn: true,
      username: username
    });
    console.log('username', username)
    console.log('username state', this.state.username)

    console.log('logged in state', this.state.loggedIn)
  }

// increments/decrements the votes in an object of lightningTalks
  incrementInApp = (id) => {
    // creates a new array based off current state of lightningTalks
    const nextLightningTalks = this.state.lightningTalks.map((currentLightningTalk) => {
      // if the id in the parameters equals the id of the current objects ID then place back into the array
      if (currentLightningTalk.id !== id) {
        return currentLightningTalk
      }
      // whatever remains (the one whose ID does match), += 1 votes of that object
        const nextLightningTalk = {...currentLightningTalk, votes: currentLightningTalk.votes + 1,
        };
    return nextLightningTalk
    })
    // sorts when number of votes increases
    nextLightningTalks.sort((a, b) => b.votes - a.votes)
// set new state of lightningTalks to equal the result of the new array above (the .map)
  this.setState({lightningTalks: nextLightningTalks})
  }

  decrementInApp = (id) => {
    const nextLightningTalks = this.state.lightningTalks.map((currentLightningTalk) => {
      if (currentLightningTalk.id !== id) {
        return currentLightningTalk
      }
        const nextLightningTalk = {...currentLightningTalk, votes: currentLightningTalk.votes - 1,
        };
    return nextLightningTalk
    })
     // sorts when number of votes decreases
    nextLightningTalks.sort((a, b) => b.votes - a.votes)

  this.setState({lightningTalks: nextLightningTalks})
  }

  lightningTalkRender(props) {
    return <div className="talks">
      {this.state.lightningTalks.votes}
        {this.state.lightningTalks.map((talk) => {
          return <LightningTalk lightningTalk={talk} incrementInApp={this.incrementInApp} decrementInApp={this.decrementInApp}/>
                })}
            </div>
  }

  // now the state of lightning talks depends on what is on the API. Below there is a loop(.map) which is set by componentDidMount
  render() {
    return (
      <div className="container">
        <h1>Lightning Talks!</h1>
          <Login login={this.login}/>
            {this.state.loggedIn ? <lightningTalkRender/> : null}
          <h3 className="form-header"> Submit your talk</h3>
        <Form postInApp={this.postInApp}/>
      </div>
    )
  }
}

export default App;


import React from "react"
import './login.css';

class Login extends React.Component {
  constructor(props) {
    super(props);
    this.loginSubmit = this.loginSubmit.bind(this)
      this.state = {
        username: '',
        password: '',
        errors: [],
        pwdStrength: null
      }
  }

  showValidationErr (e, msg) {
    this.setState((prevState) => ( { errors: [...prevState.errors, { e, msg }] } ));
  }

  clearValidationErr (e) {
    this.setState((prevState) => {
      let newArr = [];
      for(let err of prevState.errors) {
        if(e !== err.e) {
          newArr.push(err);
        }
      }
      return {errors: newArr};
    })
  }

   onUsernameChange= (e) => {
    this.setState({ username: e.target.value })
    this.clearValidationErr("username");
   }

   onPasswordChange= (e) => {
    this.setState({ password: e.target.value })
    this.clearValidationErr("password");
    // set state of password strength based on length. Render these as CSS below
    if (e.target.value.length <= 8) {
      this.setState({ pwdStrength: "pwd-weak"})
    } if (e.target.value.length > 8) {
      this.setState({ pwdStrength: "pwd-medium" })
    } if (e.target.value.length > 12) {
      this.setState({ pwdStrength: "pwd-strong" })
    }
  }

 // on submit, time is logged (new Date) and state of title and description is changed
   loginSubmit= (e) => {
    e.preventDefault()

      if(this.state.username === "") {
        this.showValidationErr("username", "Username cannot be empty")
      } if (this.state.password === "") {
        this.showValidationErr("password", "Password cannot be empty")
      }


    const username = this.state.username
    this.props.login(username)
    // call onSubmit in LightningTalk so that new talk is added from form

    // this.props.postInApp(usernamePassword)
   }
   render() {

    let usernameErr = null;
    let passwordErr = null;

    for(let err of this.state.errors) {
      if(err.e === "username") {
        usernameErr = err.msg
      } if (err.e === "password") {
        passwordErr = err.msg
      }
    }

    return (
      <form className="form-container">
        <label>
        <p className="form-title">Username:</p>
          <input className="input-username"
          placeholder="enter your username"
          value={this.state.username}
          onChange={this.onUsernameChange}
          />
          <small className = "danger-error"> { usernameErr ? usernameErr : "" }</small>
        </label>
        <br />
        <label>
        <p className="form-description">Password:</p>
          <input className="input-password"
          placeholder="enter your password"
          value={this.state.password}
          onChange={this.onPasswordChange}
          type="password"
          />
          <small className="danger-error"> { passwordErr ? passwordErr : "" }</small>
          {this.state.password && <div className="password-state">
            <div
              className={"pwd " + (this.state.pwdStrength)}></div>
          </div>}
        {/*when the button is clicked, call the loginSubmit function above. E (event) is passed into loginSubmit function (above)*/}
        </label>
        <br />
        <button onClick={e => this.loginSubmit(e)}>Login</button>
      </form>
      );
    }
}

export default Login;

Upvotes: 0

Views: 478

Answers (3)

kooskoos
kooskoos

Reputation: 4859

setState is async.If you want to see the state after setting it use a callback function

login = (username) => {
    console.log('username', username)
    console.log('username state', this.state.username)

    this.setState({
      loggedIn: true,
      username: username
    }, () => {
      console.log('username', username)
      console.log('username state', this.state.username)

      console.log('logged in state', this.state.loggedIn)        
    });
  }

Login function takes in a parameter, so the correct way to pass it as a prop is

<Login login={(username) => this.login(username)}/>

Upvotes: 2

Ashutosh Kumar
Ashutosh Kumar

Reputation: 907

hillybob991, As stated in ReactJS Docs. The setState() function is an Async Function and thus your code continues to your log statement, even before the setState is executed.

loginSubmit= (e) => {
e.preventDefault()

  if(this.state.username === "") {
    this.showValidationErr("username", "Username cannot be empty")
  } if (this.state.password === "") {
    this.showValidationErr("password", "Password cannot be empty")
  }


const username = this.state.username
this.props.login(username) //THIS LINE WOULD BE EXECUTED EVEN THOUGH YOUR SETSTATE IS NOT EXECUTED

So you are supposed to add a callback function in your code. The following code is exactly what you want.

login = (username) => {
console.log('username', username)
console.log('username state', this.state.username)

this.setState({
  loggedIn: true,
  username: username
}, () => {
  //WHATEVER YOU WANT TO BE EXECUTED AFTER SETTING STATE..
  //MAY BE YOUR CONSOLE LOG STATEMENT TOO
  const username = this.state.username
  this.props.login(username)

});

}

Upvotes: 0

JMadelaine
JMadelaine

Reputation: 2964

login = (username) => {
  this.setState({
    loggedIn: true,
    username: username
  });
  console.log('username state', this.state.username)
  console.log('logged in state', this.state.loggedIn)
}

You expect the state to have the new values when you console log above, but that is not how state works in React. React batches state updates, so when you call setState, the state is not updated immediately. Inside the login function, state.loggedIn will always have the old value. You have to wait for a rerender before the value is changed.

Is your component rerendering? Or are you assuming that state is not updating because when you console log, the state has the old value?

Try putting a console log in the top of your render function, and see if the new state gets logged correctly there, as the component will rerender with the new state.

Upvotes: 0

Related Questions