setState not setting when called from child component

componentwillreceiveprops
this.setstate is not a function
getderivedstatefromprops example
react hooks
react functional components
react ref
react state
react props

I have a simple app which fetches some weather JSON and displays it. The user can either enter a location or they can hit a "Get lucky" button, which fetches a random city. the initial state is set in App.js

    this.state = {
      error: '',
      status: '',
      queryString: 'london,gb',
      queryID: '',
      queryType: 'q',
      cityData: cityData, 
      weatherData: {},
      isLoaded: false
    }

Next, I have my main App class, then I have a child component called that contains the form gubbins. I call it in app render as follows:

      <SearchForm
        queryString={this.state.queryString}
        handleChange={this.handleChange}
        setQueryType={this.setQueryType}
        setQueryID={this.setQueryID}
        getWeatherData={this.getWeatherData}
      />

I use callback functions in there to set the query type (location or ID). An example of one of the call back functions in App.js is:

  setQueryType = (queryType) => {
    this.setState({
      queryType: queryType
    })
  }

This is called in the form JS using:

 props.setQueryType(e.target.attributes.query.value)

Now, here is the crux of the issue: the state doesn't update the first time, but DOES on the second click? In fact, other vars like queryString set in the fetch are not set until the second click.

App.js

import React, { Component } from 'react';
import './css/App.css';
import WeatherCard from './components/WeatherCard'
import Header from './components/Header'
import SearchForm from './components/SearchForm'
import cityData from './json/city.list'

const config = {
  API: 'https://api.openweathermap.org/data/2.5/forecast',
  API_KEY: process.env.REACT_APP_OPEN_WEATHER_MAP_API_KEY
}

class App extends Component {

  constructor() {
    super()
    this.state = {
      error: '',
      status: '',
      queryString: 'london,gb',
      queryID: '',
      queryType: 'q',
      cityData: cityData, 
      weatherData: {},
      isLoaded: false
    }

    this.getWeatherData()
  }

  getWeatherData = (searchValue="london,gb") => {
    let URL
    URL = config.API + '?' + this.state.queryType + '='
    URL += this.state.queryType === 'q' ? searchValue : this.state.queryID
    URL += '&units=metric&APPID=' + config.API_KEY

    console.log(URL)

    fetch(URL)
      .then( result => result.json() )
      .then ( 
        (result) => {
          if ( result.cod === '200') {
            this.setState({ 
              status: result.cod,
              weatherData: result,
              queryString: result.city.name,
              isLoaded: true
            })
          } else {
            this.setState({
              status: result.cod,
              error: result.message,
              isLoaded: false
            })
          }
      },
      (error) => {
        this.setState({
          isLoaded: false,
          error: error
        })
      }
    )
    console.log(this.state.queryString)
  }

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

  getWeatherCards = () => {
    let cards = []
    for (let i = 0; i < this.state.weatherData.cnt; i++) {
      cards.push(
        <WeatherCard 
          key={i} 
          weatherList={this.state.weatherData.list[i]} 
        />
      )
    }
    return cards
  }

  setQueryType = (queryType) => {
    this.setState({
      queryType: queryType
    })
  }

  setQueryID = () => {
    let randomID = Math.floor(Math.random() * this.state.cityData.length)
    let randomCityID = this.state.cityData[randomID].id

    this.setState({
      queryID: randomCityID
    })
  }

  getlocationForm = () => {
    return(
      <SearchForm
        queryString={this.state.queryString}
        handleChange={this.handleChange}
        setQueryType={this.setQueryType}
        setQueryID={this.setQueryID}
        getWeatherData={this.getWeatherData}
      />
    )
  }

  render = () => {
    if (this.state.status !== '200') {
      return (
        <div className='App'>
          <Header 
            status={this.state.status}
            error={this.state.error}
          />
          {this.getlocationForm()}
        </div>
      )
    } else {
      return (
        <div className='App'>
          {
            this.state.isLoaded && (
              <Header 
                cityName={this.state.weatherData.city.name} 
                countryName={this.state.weatherData.city.country} 
                status={this.state.status}
                error={this.state.error}
              />
            )
          }
          {this.getlocationForm()}
          {
            this.state.isLoaded && (
              <div className='weather-cards'>
                {this.getWeatherCards()}
              </div>
            )
          }
        </div>
      )
    }
  }
}

export default App;

SearchForm.js

import React from 'react'

const SearchForm = (props) => {

  let handleChange = function(e) {
    props.handleChange(e)
  }

  let handleClick = function(e) {
    e.preventDefault()

    props.setQueryType(e.target.attributes.query.value)

    if (e.target.attributes.query.value === 'id') { 
      props.setQueryID()
    } 

    props.getWeatherData()
  }

  return (
    <div>
      <form className="search-form">
        <input 
          type="text" 
          id="query"
          name="query" 
          placeholder="Enter a location..."
          onChange={handleChange} 
        /> 
        <button 
          type="submit" 
          query="q" 
          onClick={handleClick}
        >
          Submit
        </button>
        <button 
          type="submit" 
          query="id" 
          onClick={handleClick}
        >
          I'm feeling lucky...
        </button>
      </form>
    </div>
  )
}

export default SearchForm

I think the problem comes from the fact that when you call getWeatherData, you don't know if the setState will be over as it is an asynchronous method. (as you can see in the documentation)

So the best way, to ensure that the setState is done before calling your method without being certain of the state of your component, would be to use the callBack parameter of the setState to ensure it runs after the setState method has been finished.

How to access childs state from parent component in React, You can also use useRef to get the initial state from a child component. setState() , i'm also passing a function as a second parameter. In that callback function, I'm checking if the React parent component provided a prop called onChange() , and if so, pass the This certainly NOT recommended, but it's a nice to know. When I call the output method from the same component, it works perfectly well, and the state is changed successfully. But when I call it from my ChildrenComponent it doesn't work, and the state never changes. So I've added the following code to the output function in order to find out what's going on: console.log(this.setState).

In your App.js constructor add this.setQueryType = this.setQueryType.bind(this)

That line will bind the context of this to the current component, so when called from a child, will update parent state.

(React) setState not updating - JavaScript, Hi friends, setState is not updating values in the state for some reason from my pause function but setState is not setting the state even though all other The constructor for a React component is called before it is mounted. Trying to pass data from parent to child and then from child to parent is going to� Generally, the react component has been removed from the DOM, but some asynchronous operations we have done in the component have not finished, such as interface call. When it is completed, the set state operation is executed.

try to put your this.getWeatherData() into the componentDidMount and remove it from the constructor

componentDidMount() {
   this.getWeatherData()
}

Updating state from child component onclick - JavaScript, So, I am just trying to get the skeleton working…well idea of the skeleton for the Now I am trying to update the state from the child component… input to a property named recipe using setState, but input is not defined in the� It doesn’t matter how many setState() calls in how many components you do inside a React event handler, they will produce only a single re-render at the end of the event. For example, if child and parent each call setState() when handling a click event, the child would only re-render once.

React.Component – React, You should not call setState() in the constructor() . You can then force a component to “reset” its internal state by changing its key Note that returning false does not prevent child components from re-rendering when their state changes. Calling this.setState multiple times only re-renders the component once, the value of nextState (inside shouldComponentUpdate which is also called once only) is equal to value passed to setState on the first call. Expected behavior. shouldComponentUpdate is called each time this.setState is called & the component re-renders if it returns true

Lifting State Up – React, Often, several components need to reflect the same changing data. We will start with a component called BoilingVerdict . It accepts setState({temperature: e.target.value}); } render() { const temperature However, now that the temperature is coming from the parent as a prop, the TemperatureInput has no control over it. That’s why state is often called local or encapsulated. It is not accessible to any component other than the one that owns and sets it. When you setState a prop and use it in your component, you’re breaking the flow of the rendering props. If for some reason the prop passed into your component changed in the parent component, the child will not re-render auto-magically ?!

Setting state for parent from within useEffect hook in child , js you must take care not to call setState or dispatch (this applies as well to useReducer ) inside a useEffect hook from within a child component. setState calls are not guaranteed to be applied immediately. There are two forms of setState: one takes an object, and the other takes a function. If your setState relies on state or props values, you need to use the functional form. Never refer to this.state or this.props inside your setState.

Comments
  • You don't know if the state will be updated when you call getWeatherData after calling setQueryType. setState is asynchronous and you do not use the callback to make sure the queryType is updated before fetching the new WeatherData. Even though I don't think it answers your question, still be aware of that.
  • Actually @DimitriBosteels I'd argue I DO know the state will be updated because the fetch will either succeed or fail and update the state accordingly after the callbacks have run. Perhaps you could be a little clearer on what you think I should do?
  • I don't think this answers your question, but...I see that in your App constructor you are calling getWeatherData - usually you would do this in your componentDidMount method instead of your constructor. As per React documentation: "...componentDidMount() is invoked immediately after a component is mounted (inserted into the tree). Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request." - reactjs.org/docs/react-component.html#componentdidmount
  • One more thing to know - your first call to getWeatherData may not have returned to set the state when you click the button. You should probably pass a property to your child while your getWeatherData method is busy so that the child form can disable the Submit button. Add a new state property isLoading. Make sure to set isLoading to true before you make your async call and set it to false when done.
  • Woohhoo! You nailed it. It was the async stuff. Once I cleaned up all those functions, condensed them to one and used a callback, it worked!! Bravo :) @DimitriBosteels
  • Thanks, this actually helped another issue I was having, but I still have to click at least twice for the state to update and the query to run properly
  • Thanks, this didn't solve it but is a much better approach