React项目中useEffect获取天气数据的用法排查及相关bug修复求助
我是刚学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




