You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

React项目中useEffect获取天气数据的用法排查及相关bug修复求助

求助:React useEffect获取天气数据时机错误,输入部分关键词时应用崩溃

我是刚学React和API交互的新手,现在在做一个国家搜索+天气展示的小项目,碰到了useEffect使用的问题:

  • 当输入"fin"时,weather对象还是空的,但此时搜索结果已经是芬兰(1个结果)了
  • 输入"finl"之后,才会加载赫尔辛基的天气数据(注:因为API额度用完了,返回的success是false,但至少能拿到响应)
  • 如果取消注释代码里获取温度的那一行,输入"fin"时直接就崩溃了

希望大家帮我排查下useEffect的用法问题,修复这些bug,让天气数据能正常加载,同时避免崩溃。

我的代码如下:

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

const App = () => {
  const [ countries, setCountries ] = useState([])
  const [ search, setSearch ] = useState('')
  const [ weather, setWeather ] = useState({})
  const [ capital, setCapital ] = useState('')
  const api_key = process.env.REACT_APP_API_KEY

  const searchedCountries = countries.filter(country => country.name.common.toLowerCase().includes(search.toLowerCase()))

  useEffect(() => {
    axios
      .get('https://restcountries.com/v3.1/all')
      .then( response => {
        setCountries(response.data)
      })
  },[])

  useEffect(() => {
    const url = `http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`
    axios
      .get(url)
      .then(response => {
        setWeather(response.data)
        console.log('logged weather', response.data)
        console.log('capital inside: ', capital)
      })
  },[search])

  console.log(searchedCountries)

  const handleSearch = (event) => {
    setSearch(event.target.value)
    if(searchedCountries.length === 1){
      setCapital(searchedCountries[0].capital[0])
      console.log('capital is set', capital)
    } else setCapital('')
  }

  const handleShow = (event) => {
    event.preventDefault()
    setSearch(event.target.value)
  }

  if(searchedCountries.length > 10){
    return(
      <>
        <div>
          find countries
          <input value = {search} onChange = {handleSearch} />
        </div>
        <p>Too many matches, specify another filter</p>
      </>
    )
  }else{
    if(searchedCountries.length === 1){
      const languages = Object.keys(searchedCountries[0].languages)
      console.log(typeof weather, weather)
      return(
        <>
          <div>
            find countries
            <input value = {search} onChange = {handleSearch} />
          </div>
          <h1>{searchedCountries[0].name.common}</h1>
          <p>capital {searchedCountries[0].capital[0]}</p>
          <p>population {searchedCountries[0].population}</p>
          <h2>languages</h2>
          <ul>
            {languages.map(language => <li key = {language}>{searchedCountries[0].languages[language]}</li>)}
          </ul>
          <img src= {searchedCountries[0].flags.png} alt= 'flag' />
          <h2>Weather in {searchedCountries[0].capital[0]}</h2>
          {/* <p><strong>temperature: </strong> {weather.current.temperature} Celcius</p> */}
        </>
      )
    }else{
      return(
        <>
          <div>
            find countries
            <input value = {search} onChange = {handleSearch} />
          </div>
          {searchedCountries.map( (country, index) => {
            return(
              <div key = {country.name.official}>{country.name.common}
                <button onClick = {handleShow} value = {country.name.common} >show</button>
              </div>
            )
          })}
        </>
      )
    }
  }
}

export default App

问题分析与修复方案

我帮你梳理了几个核心问题,以及对应的修复步骤:

1. useEffect获取天气的依赖项错误

你现在的天气请求useEffect依赖的是search,但search变化时,searchedCountries可能还没更新(因为setState是异步的),而且只有当搜索结果是1个国家时才需要请求天气。

修复:
把天气请求的useEffect依赖改成capital,并且只有当capital不为空时才发起请求:

useEffect(() => {
  if (!capital) return; // 为空时不请求
  const url = `http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`;
  axios
    .get(url)
    .then(response => {
      setWeather(response.data);
      console.log('logged weather', response.data);
    })
    .catch(err => {
      // 处理API请求失败的情况,比如额度用完
      console.error('天气API请求失败:', err);
      setWeather({ error: '无法获取天气数据' });
    });
}, [capital, api_key]); // 添加api_key到依赖(如果它可能变化的话)

2. handleSearch中searchedCountries的状态滞后

setSearch是异步操作,调用它之后,searchedCountries还是更新前的旧值,所以此时判断searchedCountries.length === 1是错误的。

修复:
不要在handleSearch里判断,而是用一个useEffect监听searchedCountries的变化,当它长度为1时自动设置capital:

// 新增这个useEffect,监听搜索结果的变化
useEffect(() => {
  if (searchedCountries.length === 1) {
    setCapital(searchedCountries[0].capital[0]);
  } else {
    setCapital('');
    setWeather({}); // 清空之前的天气数据
  }
}, [searchedCountries]);

// 修改handleSearch,只负责更新search
const handleSearch = (event) => {
  setSearch(event.target.value);
};

3. 未处理weather为空/请求失败的情况

当weather还没加载或者请求失败时,直接访问weather.current.temperature会导致崩溃,因为weather.current可能不存在。

修复:
使用可选链操作符?.和空值合并操作符??来做安全访问:

<p><strong>temperature: </strong> {weather.current?.temperature ?? '暂无数据'} Celcius</p>

如果API返回success: false,你也可以根据这个状态显示错误信息:

{weather.success === false ? (
  <p>抱歉,无法获取天气数据:{weather.error?.info}</p>
) : (
  <p><strong>temperature: </strong> {weather.current?.temperature ?? '暂无数据'} Celcius</p>
)}

4. handleShow按钮的小问题

点击"show"按钮时,event.preventDefault()是多余的(因为不是表单提交),而且直接设置search为国家名称可能会匹配到多个结果(比如有重名的),最好直接设置search为唯一能匹配到该国家的值,或者直接通过国家对象来设置capital,但更简单的是把按钮的点击逻辑改成直接设置search为精确的国家名称:

const handleShow = (countryName) => {
  setSearch(countryName);
};

// 在渲染按钮时:
<button onClick={() => handleShow(country.name.common)}>show</button>

修复后的完整代码

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

const App = () => {
  const [ countries, setCountries ] = useState([])
  const [ search, setSearch ] = useState('')
  const [ weather, setWeather ] = useState({})
  const [ capital, setCapital ] = useState('')
  const api_key = process.env.REACT_APP_API_KEY

  const searchedCountries = countries.filter(country => country.name.common.toLowerCase().includes(search.toLowerCase()))

  useEffect(() => {
    axios
      .get('https://restcountries.com/v3.1/all')
      .then( response => {
        setCountries(response.data)
      })
  },[])

  // 监听搜索结果变化,设置首都或清空
  useEffect(() => {
    if (searchedCountries.length === 1) {
      setCapital(searchedCountries[0].capital[0]);
    } else {
      setCapital('');
      setWeather({});
    }
  }, [searchedCountries])

  // 根据首都获取天气数据
  useEffect(() => {
    if (!capital) return;
    
    const url = `http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`;
    axios
      .get(url)
      .then(response => {
        setWeather(response.data);
        console.log('logged weather', response.data);
      })
      .catch(err => {
        console.error('天气API请求失败:', err);
        setWeather({ error: '无法获取天气数据,请检查API密钥或额度' });
      });
  }, [capital, api_key])

  const handleSearch = (event) => {
    setSearch(event.target.value);
  }

  const handleShow = (countryName) => {
    setSearch(countryName);
  }

  if(searchedCountries.length > 10){
    return(
      <>
        <div>
          find countries
          <input value={search} onChange={handleSearch} />
        </div>
        <p>Too many matches, specify another filter</p>
      </>
    )
  }else if(searchedCountries.length === 1){
    const country = searchedCountries[0];
    const languages = Object.values(country.languages); // 直接取语言值,更简洁
    
    return(
      <>
        <div>
          find countries
          <input value={search} onChange={handleSearch} />
        </div>
        <h1>{country.name.common}</h1>
        <p>capital {country.capital[0]}</p>
        <p>population {country.population.toLocaleString()}</p> {/* 格式化人口数字 */}
        <h2>languages</h2>
        <ul>
          {languages.map(lang => <li key={lang}>{lang}</li>)}
        </ul>
        <img src={country.flags.png} alt={`${country.name.common} flag`} style={{width: '150px'}} />
        <h2>Weather in {country.capital[0]}</h2>
        {weather.success === false ? (
          <p><strong>错误:</strong> {weather.error?.info}</p>
        ) : weather.error ? (
          <p><strong>错误:</strong> {weather.error}</p>
        ) : (
          <>
            <p><strong>temperature: </strong> {weather.current?.temperature ?? '暂无数据'} Celcius</p>
            <img src={weather.current?.weather_icons?.[0]} alt="weather icon" />
            <p><strong>wind: </strong> {weather.current?.wind_speed ?? 0} mph direction {weather.current?.wind_dir ?? '-'}</p>
          </>
        )}
      </>
    )
  }else{
    return(
      <>
        <div>
          find countries
          <input value={search} onChange={handleSearch} />
        </div>
        {searchedCountries.map( country => (
          <div key={country.name.official} style={{margin: '8px 0'}}>
            {country.name.common}
            <button onClick={() => handleShow(country.name.common)} style={{marginLeft: '8px'}}>show</button>
          </div>
        ))}
      </>
    )
  }
}

export default App

这些修改应该能解决你的问题:现在当搜索结果变成1个国家时,会自动请求对应首都的天气;访问天气数据时做了安全检查,不会崩溃;API请求失败时也会友好提示错误信息。

内容的提问来源于stack exchange,提问作者spademarck

火山引擎 最新活动