React中使用useEffect、axios和setState时,无法重置书籍状态为空数组的问题
React中使用useEffect、axios和setState时,无法重置书籍状态为空数组的问题
嘿,我来帮你捋清楚这个问题~ 其实核心原因是React的状态更新是异步的,而且useEffect的执行时机是在组件渲染完成之后,你看到的日志只是组件第一次渲染的结果,咱们一步步拆解:
问题到底出在哪?
当你清空搜索框时,整个流程是这样的:
- 触发
setSearch(""),组件开始第一次重新渲染 - 这时候
search已经是空字符串了,但books还是之前请求回来的旧数组(因为useEffect里的setBooks([])还没执行) - 同步代码里的
filteredBooks会因为search === ""变成空数组,所以你看到的日志里filteredBooks是空,但books还是旧数据 - 组件渲染完成后,
useEffect才会执行,里面的setBooks([])才会触发状态更新,这时候组件会进行第二次重新渲染 - 第二次渲染时,
books已经是空数组了,但你可能没注意到这次的日志(浏览器控制台会打印两次,但你可能只看了第一次)
你的filteredBooks做法虽然能让页面显示正确,但其实是绕开了状态更新的异步特性,不是最优雅的解法。
优化方案:去掉冗余变量,利用状态逻辑+解决竞态问题
首先,咱们不需要额外的filteredBooks,直接用books状态结合search来判断渲染内容就行。另外还要处理请求竞态的问题——比如你快速输入“react”又马上清空,之前的搜索请求还没完成,这时候它的响应会覆盖掉你设置的空数组,导致books又变回旧数据。
可以用AbortController来取消未完成的请求,修改后的代码如下:
import React, { useState, useEffect } from 'react'; import Book from './components/Book'; import axios from 'axios' const App = () => { const [search, setSearch] = useState("") const [books, setBooks] = useState([]) useEffect(() => { // 创建AbortController实例,用于取消请求 const abortController = new AbortController(); if (search === "") { setBooks([]) return } async function fetchData() { try { const response = await axios({ url: `https://www.googleapis.com/books/v1/volumes?q=${search}&key=${process.env.REACT_APP_API_GOOGLE_FAKE}`, signal: abortController.signal // 绑定信号 }) setBooks(response.data.items) } catch (exception) { // 忽略取消请求的错误 if (!axios.isCancel(exception)) { console.log(exception) } } } fetchData() // 组件卸载或search变化时,取消未完成的请求 return () => abortController.abort(); }, [search]) const handleSearch = (event) => { setSearch(event.target.value) } console.log(search, 'search...'); console.log(books, 'books...'); return ( <div className="App"> <header className="App-header"> <nav> <ul> <li>Your menu</li> </ul> </nav> </header> <section> <article> <div className="input-group searchListBooks"> <div className="form-outline"> <input onChange={handleSearch} type="search" id="form1" className="form-control" value={search} /> <label className="form-label" htmlFor="form1">Search</label> </div> </div> </article> <article> <header> <h2>Books</h2> </header> <ul className="listBook"> { search.length === 0 ? <p>Write some text</p> : books.length === 0 ? <p>Loading...</p> {/* 可以加个加载提示 */} : books.map(book => ( <Book {...book} key={book.id} /> )) } </ul> </article> </section> <aside> <h2>About section</h2> <p>Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p> </aside> <footer> <p>Copyright 2009 Your name</p> </footer> </div> ); } export default App;
关键优化点说明:
- 去掉了
filteredBooks:直接用books状态判断,当search为空时显示提示,search不为空但books为空时可以显示加载状态,更符合用户体验 - 添加了请求取消逻辑:用
AbortController在search变化或组件卸载时取消未完成的请求,彻底避免竞态问题,保证清空搜索框时books一定会被重置为空数组 - 简化了
useEffect的依赖数组:原来的依赖数组末尾多了个逗号,虽然不影响,但去掉更规范
现在你再清空搜索框,就能看到books在第二次渲染时变成空数组了,而且页面也能正确显示提示内容~
备注:内容来源于stack exchange,提问作者titoih
相关产品推荐
相关产品推荐

