React中使用回调更新state为何需两次点击才生效?
问题:点击两次才更新
filteredBooks状态 尽管使用回调函数更新状态,组件中的filteredBooks状态仍需要点击两次才能完成更新,相关代码如下:
import { useRef, useEffect, useState } from "react"; import "./App.css"; import Card from "./UI/Card"; function App() { const [books, setBooks] = useState([]); const [filteredBooks, setFilteredBooks] = useState([]); const [yearRange, setYearRange] = useState([]); const filterBksByYearRange = () => { const filtered = books.filter( (book) => book.year >= yearRange[0] && book.year <= yearRange[1] ); setFilteredBooks(() => filtered); }; const handleYrRange = (e) => { e.preventDefault(); const yearStart = parseInt(e.target[0].value); const yearEnd = parseInt(e.target[1].value); console.log(yearStart, yearEnd); setYearRange(() => [yearStart, yearEnd]); // SOLVE: books complete list only at initial fetch. if multiple filters applied, will not reflect accurate filter filterBksByYearRange(); }; useEffect(() => { const url = "/books"; const fetchData = async () => { try { const response = await fetch(url); const json = await response.json(); console.log(json); setBooks((prev) => [prev, ...json.body].map((book) => { return { title: book.title, author: book.author, year: parseInt(book.year), isbn: parseInt(book.isbn), }; }) ); } catch (error) { console.log("error", error); } }; fetchData(); }, []); return ( <div className="App"> <h1>ServicePros Coding Challenge</h1> <form onSubmit={handleYrRange}> <label> Start Year: <input type="text" name="yearStart" placeholder="" /> </label> <label> Start End: <input type="text" name="yearEnd" placeholder="" /> </label> <button type="submit">Filter by Year</button> </form> <ul> {filteredBooks.length > 0 ? ( filteredBooks.map((book) => ( <Card key={Math.random()}> <li> <div>Title: {book.title}</div> <div>Author: {book.author}</div> <div>Year: {book.year}</div> <div>ISBN: {book.isbn}</div> </li> </Card> )) ) : ( <p>No Books Filtered</p> )} </ul> </div> ); } export default App;
问题原因
React的状态更新是异步的,调用setYearRange后,yearRange不会立即更新为新值。在handleYrRange中直接调用filterBksByYearRange时,函数内使用的还是旧的yearRange,导致第一次过滤逻辑无效;第二次点击时,yearRange已经更新为上次的输入值,过滤才会生效。
解决方案
方案1:用useEffect监听yearRange变化自动过滤
移除handleYrRange中对filterBksByYearRange的直接调用,改用useEffect监听yearRange,当范围更新时自动执行过滤:
import { useRef, useEffect, useState } from "react"; import "./App.css"; import Card from "./UI/Card"; function App() { const [books, setBooks] = useState([]); const [filteredBooks, setFilteredBooks] = useState([]); const [yearRange, setYearRange] = useState([]); // 过滤逻辑 const filterBksByYearRange = () => { if (!yearRange[0] || !yearRange[1]) return; const filtered = books.filter( (book) => book.year >= yearRange[0] && book.year <= yearRange[1] ); setFilteredBooks(filtered); }; // 监听yearRange变化,自动过滤 useEffect(() => { filterBksByYearRange(); }, [yearRange, books]); const handleYrRange = (e) => { e.preventDefault(); const yearStart = parseInt(e.target[0].value); const yearEnd = parseInt(e.target[1].value); setYearRange([yearStart, yearEnd]); }; useEffect(() => { const url = "/books"; const fetchData = async () => { try { const response = await fetch(url); const json = await response.json(); // 修正初始书籍状态更新,避免嵌套数组 const formattedBooks = json.body.map((book) => ({ title: book.title, author: book.author, year: parseInt(book.year), isbn: parseInt(book.isbn), })); setBooks(formattedBooks); } catch (error) { console.log("error", error); } }; fetchData(); }, []); return ( <div className="App"> <h1>ServicePros Coding Challenge</h1> <form onSubmit={handleYrRange}> <label> Start Year: <input type="text" name="yearStart" placeholder="" /> </label> <label> End Year: <input type="text" name="yearEnd" placeholder="" /> </label> <button type="submit">Filter by Year</button> </form> <ul> {filteredBooks.length > 0 ? ( filteredBooks.map((book) => ( <Card key={book.isbn}> {/* 用唯一的isbn作为key,避免Math.random()的不稳定 */} <li> <div>Title: {book.title}</div> <div>Author: {book.author}</div> <div>Year: {book.year}</div> <div>ISBN: {book.isbn}</div> </li> </Card> )) ) : ( <p>No Books Filtered</p> )} </ul> </div> ); } export default App;
方案2:直接使用最新的输入值过滤
不需要依赖yearRange状态,直接在handleYrRange中用获取到的yearStart和yearEnd进行过滤,跳过异步状态更新的等待:
const handleYrRange = (e) => { e.preventDefault(); const yearStart = parseInt(e.target[0].value); const yearEnd = parseInt(e.target[1].value); setYearRange([yearStart, yearEnd]); // 直接用最新的年份范围过滤 const filtered = books.filter( (book) => book.year >= yearStart && book.year <= yearEnd ); setFilteredBooks(filtered); };
额外优化点
- 初始书籍状态更新时,原代码会把
prev数组和新数组合并导致嵌套数组,修正为直接格式化新获取的书籍列表。 - 组件的
key用唯一的isbn代替Math.random(),避免组件重复渲染或状态丢失问题。 - 表单标签中的"Start End"修正为"End Year",语义更准确。
内容的提问来源于stack exchange,提问作者brunshte
相关产品推荐
相关产品推荐

