4

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
studstill
  • 65
  • 1
  • 10
  • What I personally find more fail safe when it comes to state management is a "reducer" workflow, like Redux or -my preference- useReducer which is part of React itself. Definitely worth a read: https://reactjs.org/docs/hooks-reference.html#usereducer – Ozone Oct 01 '20 at 18:31
  • try this -- `setWeather({is: data} )` – Sarun UK Oct 01 '20 at 18:31
  • Thanks Ozone, will do. @Saran: Thanks, yep, I see. Doesn't address the larger problem, but that's one thing down! – studstill Oct 01 '20 at 20:58

1 Answers1

2

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]);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Gotcha, and thanks! The two argument object was an attempt by me to further address the not-updating-state issue (creating and then flipping the "is" value), so I got rid of it like you advised. Further thanks for that note! Implementing the other recommendations has left me with an infinite loop, but that's progress, it's now changing the state and rerendering, at least! – studstill Oct 01 '20 at 20:56
  • @chrisstudstill The `useEffect` in `CountryFound` has `newWeather` as a dependency (for the console log), but you update `newWeather` via the axios call and `handleWeather` and create a new object reference. This is certainly likely to cause render looping. – Drew Reese Oct 01 '20 at 21:02
  • @chrisstudstill If the updating state not rerendering issue is fixed then please accept an answer so others know it is resolved. – Drew Reese Oct 01 '20 at 21:13
  • Yeah, will do. Should I do that now? The only way the state is updating is by forming the infinite loop, but I think I am still testing now, updated the code in OP. – studstill Oct 01 '20 at 22:52
  • In your updated code you completely removed the dependency array from the effect in `CountryFound ` that updates the weather state, so the effect will run every render. Looks like you may need this dependency: `[countryFound.capital, api_key, handleWeather]`, or at a minimum, an empty dependency array (`[]`) to do the fetch only once when that component is mounted (I haven't traced fully through your code to know for sure if `CountryFound` is remounted, though I think it is). – Drew Reese Oct 02 '20 at 15:20