简体   繁体   中英

setState not setting when called from child component

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

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.

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

componentDidMount() {
   this.getWeatherData()
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM