React:setState没有导致重新渲染?

4

我是React、JS、JSX的新手。

似乎setNewWeather没有正确地更新天气状态,也就是说,它被定义为初始值,但后来又变成了undefined。

因为如果它更新了,应该会引起重新渲染;我已经查看了很多关于此的帖子,但它们建议等待异步数据操作,但按照我的理解,使用'.then'方法天然地实现了这一点?或者这是涉及到setNewWeather语法的不同问题,比如需要在内部使用一个函数而不仅仅是一个字符串来更新状态?

我的代码:

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>
    </>)}
    */

谢谢!

编辑:状态正在更新,但只通过形成无限循环来实现:

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

编辑最终版:已解决!

下面的标记解决了最初的问题,但生成了一个无限循环。整个问题已经得到纠正,尽管我还不太理解这些更改是如何实现的。

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

个人认为在状态管理方面更加安全的是“reducer”工作流程,例如Redux或者-我更喜欢的-useReducer,它是React本身的一部分。绝对值得一读:https://reactjs.org/docs/hooks-reference.html#usereducer - Ozone
尝试这个 -- setWeather({is: data} ) - Sarun UK
谢谢Ozone,我会做的。 @Saran: 谢谢,嗯,我明白了。虽然这没有解决更大的问题,但至少解决了其中一件事! - studstill
1个回答

2

问题

handleWeather被定义为接受两个参数。

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

但是当你调用它时,只传递一个参数

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

此外,React状态更新是异步的,并且在渲染周期之间批处理。因此,在更新被排队后立即尝试在控制台记录状态只会记录当前状态。
解决方案:
您应该选择接受这两个参数并创建要在状态中存储的对象,或者始终传递已经创建的要存储的对象。以下内容将使用后一种方法。
const handleWeather = (newWeather) => () => setWeather(newWeather);

注意: 目前,`handleWeather` 只是代理 `newWeather` 对象,因此可以进行一些小的优化,比如不代理,由于函数签名匹配,即 `const handleWeather = setWeather` 或者直接将 `setWeather` 作为回调函数传递。
<Countries
  countries={countries} 
  a_k1={api_key} 
  handleWeather={setWeather} // <-- directly pass state update function
  handleClick={handleClick} 
  newSearch={newSearch}
  newWeather={newWeather}
/>

使用一个效果来记录更新后的newWeather,将newWeather作为依赖项。

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

懂了,谢谢!我尝试使用双参数对象来进一步解决不更新状态的问题(创建然后翻转“is”值),所以像您建议的那样将其删除了。感谢您的提示!实施其他建议让我陷入了无限循环,但这是进步,至少现在正在改变状态并重新渲染! - studstill
@chrisstudstill 在 CountryFound 中的 useEffectnewWeather 作为依赖项(用于控制台日志),但是您通过 axios 调用和 handleWeather 更新 newWeather 并创建了一个新的对象引用。这很可能会导致渲染循环。 - Drew Reese
@chrisstudstill 如果更新状态后不重新渲染问题已经解决,请接受一个答案,以便其他人知道它已经解决。 - Drew Reese
好的,我会做。现在我应该这样做吗?状态更新的唯一方式是通过形成无限循环,但我认为我仍在测试中,在 OP 中更新了代码。 - studstill
在您更新的代码中,您完全从更新天气状态的CountryFound效果中删除了依赖数组,因此该效果将在每次渲染时运行。看起来您可能需要这个依赖项:[countryFound.capital, api_key, handleWeather],或者至少是一个空的依赖项数组([]),以便在组件挂载时仅执行一次获取操作(尽管我没有完全跟踪您的代码以确切知道CountryFound是否被重新挂载)。 - Drew Reese

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接