React: setState is not causing a re-render?

4.1k views Asked by At

I am new to React, JS, JSX.

It doesn't appear that setNewWeather is updating the weather state properly, as in, it's defined by the initial value, but then changes to undefined.

Because if it updates, it should cause the re-render; I have looked at a lot of posts about this, but they advise to like, wait on the async data operation, but it's my understanding that using the '.then' method does that inherently? Or it's a different issue involving the syntax of the setNewWeather, like it needs to use a function inside instead of just a string, to update state?

My code:

import React, { useState, useEffect } from 'react'
import axios from 'axios'

const Header = ({ text }) => <h1>{text}</h1>
const Header3 = ({ text }) => <h3>{text}</h3>
const Image = ({ source, alttext }) => <img src={source} alt={alttext} />
const Button = ({ onClick, text }) => (<button onClick={onClick}>{text}</button>)
const ListItem = ({ item }) => <li>{item}</li>
const List = ({ title, stuff }) => {
  return(
    <>
    < Header3 text={title} />
    <ul>
      {stuff.map((item, index) => < ListItem key={index} item={item} />)}
    </ul>
    </>)}
const Search = ({ text, value, onChange }) => {
  return (
    <>
    {text}
    <input value={value} onChange={onChange} />
    </>)}

const CountryMany = ({ country, handleClick }) => {
  return (
    <>
    <li>{country.name}</li>
    < Button onClick={handleClick(country.name)} text='Show' />
    </>)}

const CountryFound = ({ country, api_key, handleWeather, newWeather }) => {
  const countryFound = country[0]
  const params = {
    access_key: api_key,
    query: countryFound.capital
  }
  useEffect(() => {
    axios.get('http://api.weatherstack.com/current', {params})
         .then(response => {
          console.log('RESPONSE', response.data)
          handleWeather({ is: 'weather', data: response.data })
          console.log(newWeather)
          })},
        [params, newWeather, handleWeather])
  console.log('yo')
  console.log(newWeather)

  const languages = countryFound.languages.map(lang => lang.name)
  return (
    <>
    < Header text={countryFound.name} />
    <p>Capital: {countryFound.capital}</p>
    <p>Population: {countryFound.population}</p>
    < List title='Languages' stuff={languages} />
    < Header3 text='Flag' />
    < Image source={countryFound.flag} alttext='flag' />
    < Header3 text='Weather' />
    <ul>
      <li>Temperature: {newWeather}</li>
      <li> Image source= alttext=weather </li>
    </ul></>)}

const Countries = (props) => {
  console.log('COUNTRIES PROPS', props)
  console.log('WEATHER', props.newWeather)
  const countries = props.countries
  const foundCountries = countries.filter(country =>
    country.name.toLowerCase().includes(props.newSearch.toLowerCase()))
  if (foundCountries.length > 10 ) {
    return (<p>Too Many Matches, Keep Typing!</p>)}
  if (foundCountries.length > 1) {
    return (
        <ul>
        {foundCountries.map(country =>
        < CountryMany key={country.population} country={country} handleClick={props.handleClick} />)}
        </ul>)}
  if (foundCountries.length === 1) {
    return (
        <>
          <CountryFound api_key={props.a_k1} country={foundCountries}
            handleWeather={props.handleWeather} weather={props.newWeather} />
        </>)}
  return (<></>)}

const App = () => {
  const api_key = process.env.REACT_APP_API_KEY
  const [ countries, setCountries ] = useState([])
  const [ newSearch, setNewSearch ] = useState('')
  const [ newWeather, setWeather ] = useState({ is: 'no ewather' })
  const handleWeather = ( is, data ) => () => {
    setWeather( is, data )
    console.log('HEY HANDLEWEATHER', newWeather)}
  
  useEffect(() => {
    axios
      .get('https://restcountries.eu/rest/v2/all')
      .then(response => {
        setCountries(response.data)
      })}, [])
  
  const handleClick = (value) => () => {
          setNewSearch(value)}
  const handleSearch = (event) => {
          setNewSearch(event.target.value)}
  
  return (
    <div>
      < Search text='Find A Country: ' value={newSearch} onChange={handleSearch}/>
      < Countries countries={countries} 
                  a_k1={api_key} 
                  handleWeather={handleWeather}
                  handleClick={handleClick} 
                  newSearch={newSearch}
                  newWeather={newWeather}
                   />
    </div>)}

export default App

/*
const Weather = ({ weather }) => {
    return (
    <>
    < Header3 text='Weather' />
    <ul>
      <li>Temperature: weather.temperature</li>
      <li> Image source= alttext=weather </li>
    </ul>
    </>)}
    */

Thanks!

Edit: State is updating, but only by forming an infinite loop:

import React, { useState, useEffect } from 'react'
import axios from 'axios'

const Header = ({ text }) => <h1>{text}</h1>
const Header3 = ({ text }) => <h3>{text}</h3>
const Image = ({ source, alttext }) => <img src={source} alt={alttext} />
const Button = ({ onClick, text }) => (<button onClick={onClick}>{text}</button>)
const ListItem = ({ item }) => <li>{item}</li>
const List = ({ title, stuff }) => {
  return(
    <>
    < Header3 text={title} />
    <ul>
      {stuff.map((item, index) => < ListItem key={index} item={item} />)}
    </ul>
    </>)}
const Search = ({ text, value, onChange }) => {
  return (
    <>
    {text}
    <input value={value} onChange={onChange} />
    </>)}

const CountryMany = ({ country, handleClick }) => {
  return (
    <>
    <li>{country.name}</li>
    < Button onClick={handleClick(country.name)} text='Show' />
    </>)}

const CountryFound = ({ countryFound, api_key, handleWeather, newWeather }) => {
  const params = { access_key: api_key, query: countryFound.capital }
  useEffect(() => {
    axios.get('http://api.weatherstack.com/current', {params})
         .then(response => {
          console.log('RESPONSE', response.data)
          handleWeather(response.data)
          })})
  
  
  const languages = countryFound.languages.map(lang => lang.name)
  if (newWeather.length >  0 ){
    return (
      <>
      < Header text={countryFound.name} />
      <p>Capital: {countryFound.capital}</p>
      <p>Population: {countryFound.population}</p>
      < List title='Languages' stuff={languages} />
      < Header3 text='Flag' />
      < Image source={countryFound.flag} alttext='flag' />
      < Header3 text='Weather' />
      <ul>
      <li>Temperature/rendering {newWeather}</li>
      <li> Image source= alttext=weather </li>
      </ul></>)}
  return (
    <></>
  )}
  

const Countries = (props) => {
  console.log('COUNTRIES PROPS', props)
  console.log('WEATHER', props.newWeather)
  const foundCountries = props.countries.filter(country =>
    country.name.toLowerCase().includes(props.newSearch.toLowerCase()))
  if (foundCountries.length > 10 ) {
    return (<p>Too Many Matches, Keep Typing!</p>)}
  if (foundCountries.length > 1) {
    return (
        <ul>
        {foundCountries.map(country =>
        < CountryMany key={country.population} country={country} handleClick={props.handleClick} />)}
        </ul>)}
  if (foundCountries.length === 1) {
    return (
        <>
          <CountryFound api_key={props.a_k1} countryFound={foundCountries[0]}
            handleWeather={props.handleWeather} newWeather={props.newWeather} />
        </>)}
  return (<></>)}

const App = () => {
  const api_key = process.env.REACT_APP_API_KEY
  const [ countries, setCountries ] = useState([])
  const [ newSearch, setNewSearch ] = useState('af')
  const [ newWeather, setWeather ] = useState([])
  const handleClick = (value) => () => {
        setNewSearch(value)}
  const handleSearch = (event) => {
        setNewSearch(event.target.value)}

  useEffect(() => {
    axios
      .get('https://restcountries.eu/rest/v2/all')
      .then(response => {
        setCountries(response.data)
      })}, [])
  return (
    <div>
      < Search text='Find A Country: ' value={newSearch} onChange={handleSearch}/>
      < Countries countries={countries} 
                  a_k1={api_key} 
                  handleWeather={setWeather}
                  handleClick={handleClick} 
                  newSearch={newSearch}
                  newWeather={newWeather}
                   />
    </div>)}

export default App

Edit Final: Solved!

The marked solution below solved the inital issue, yet generated an infinite loop. I have rectified the whole thing, although I do not quite understand yet how it has all changed.

import React, { useState, useEffect } from 'react'
import axios from 'axios'

const Header = ({ text }) => <h1>{text}</h1>
const Header3 = ({ text }) => <h3>{text}</h3>
const Image = ({ source, alttext }) => <img src={source} alt={alttext} />
const Button = ({ onClick, text }) => (<button onClick={onClick}>{text}</button>)
const ListItem = ({ item }) => <li>{item}</li>
const List = ({ title, stuff }) => {
  return(
    <>
    < Header3 text={title} />
    <ul>
      {stuff.map((item, index) => < ListItem key={index} item={item} />)}
    </ul>
    </>)}

const Search = ({ text, value, onChange }) => {
  return (
    <>
    {text}
    <input value={value} onChange={onChange} />
    </>)}

const CountryMany = ({ country, handleClick }) => {
  return (
    <>
    <li>{country.name}</li>
    < Button onClick={handleClick(country.name)} text='Show' />
    </>)}

const CountryFound = ({ countryFound, api_key, handleWeather, newWeather }) => {
  useEffect(() => {
    axios.get(`https://api.weatherbit.io/v2.0/current?city=${countryFound.capital}&key=${api_key}`)
          .then(response => {
          handleWeather(response.data.data[0])
          })})
  const languages = countryFound.languages.map(lang => lang.name)
  if (newWeather > '' ) {
    const capital = countryFound.capital
    const weatherTitle = `Weather in: ${capital}`
    const weatherImage = `https://www.weatherbit.io/static/img/icons/${newWeather.weather.icon}.png`
    return (
      <>
      < Header text={countryFound.name} />
      <p>Capital: {capital}</p>
      <p>Population: {countryFound.population}</p>
      < List title='Languages' stuff={languages} />
      < Header3 text='Flag' />
      < Image source={countryFound.flag} alttext='flag' />
      < Header3 text={weatherTitle} />
      < Image source={weatherImage} alttext='weather' />
      <ul>
      <li>Temperature: {newWeather.temp} degrees Celsius</li>
      <li>Wind: {newWeather.wind_spd} mph towards {newWeather.wind_cdir}</li>
      </ul></>)}
  return (<><p>Loading...</p></>)}
  
const Countries = (props) => {
  const foundCountries = props.countries.filter(country =>
    country.name.toLowerCase().includes(props.newSearch.toLowerCase()))
  if (foundCountries.length > 10 ) {
    return (<p>Too Many Matches, Keep Typing!</p>)}
  
  if (foundCountries.length > 1) {
    return (
        <ul>
        {foundCountries.map(country =>
        < CountryMany key={country.population} 
                      country={country} 
                      handleClick={props.handleClick} />)}
        </ul>)}
  
  if (foundCountries.length === 1) {
    return (<>
          <CountryFound api_key={props.a_k1} countryFound={foundCountries[0]}
            handleWeather={props.handleWeather} newWeather={props.newWeather} />
            </>)}
  return (<></>)}

const App = () => {
  const api_key = process.env.REACT_APP_API_KEY
  const [ countries, setCountries ] = useState([])
  const [ newSearch, setNewSearch ] = useState('af')
  const [ newWeather, setWeather ] = useState('')
  const handleClick = (value) => () => {
        setNewSearch(value)}
  const handleSearch = (event) => {
        setNewSearch(event.target.value)}

  useEffect(() => {
    axios
      .get('https://restcountries.eu/rest/v2/all')
      .then(response => {
        setCountries(response.data)
      })}, [])

  return (
    <div>
      < Search text='Find A Country: ' value={newSearch} onChange={handleSearch}/>
      < Countries countries={countries} 
                  a_k1={api_key} 
                  handleWeather={setWeather}
                  handleClick={handleClick} 
                  newSearch={newSearch}
                  newWeather={newWeather}
                   />
    </div>)}

export default App
1

There are 1 answers

5
Drew Reese On BEST ANSWER

Issues

handleWeather is defined to take two arguments

const handleWeather = ( is, data ) => () => {
  setWeather( is, data )
  console.log('HEY HANDLEWEATHER', newWeather)
}

But when you call it you only pass a single argument

handleWeather({ is: 'weather', data: response.data })

Additionally, react state updates are asynchronous and batched processed between render cycles, so trying to console log state right after the update is enqueued will only log the current state.

Solution

You should settle on either accepting the two arguments and creating the object you want in state, or consistently pass it the already-created object you want to store. The following will use the latter.

const handleWeather = (newWeather) => () => setWeather(newWeather);

Note: At this point handleWeather is simply just proxying the newWeather object, so minor optimizations could be to not proxy since the function signatures match, i.e. const handleWeather = setWeather, or just directly pass setWeather as the callback.

<Countries
  countries={countries} 
  a_k1={api_key} 
  handleWeather={setWeather} // <-- directly pass state update function
  handleClick={handleClick} 
  newSearch={newSearch}
  newWeather={newWeather}
/>

Use an effect to log the updated newWeather, use newWeather as the dependency.

useEffect(() => {
  console.log('HEY HANDLEWEATHER', newWeather)
}, [newWeather]);