You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 10:00:28