React中useEffect内调用setState为何无法触发组件重渲染?
问题:商品删除成功后组件无法自动重新渲染
我已经实现商品列表展示,点击删除按钮能成功删除数据库中的商品,但触发删除操作的useEffect无法让组件重新渲染。相关代码如下:
ProductDisplay组件代码
import React, { useState, useEffect, useRef } from "react"; import { Container, ItemDescription, ProductContainer } from "./style"; import api from "../../api"; const API = "http://localhost:4000/select"; interface DATA { sku: string, name: string, price: string, type: string, size: number, weight: number, height: number, width: number, length: number, } interface IProps { del: boolean } const ProductDisplay = ({del}:IProps) => { const [productList, setProductList] = useState<Array<DATA>>([]); const [checkedItems, setCheckedItems] = useState<Array<boolean>>([]); const [deleteList, setDeleteList] = useState<Array<any>>([]); const renderBlock = useRef(0) // will change this to a boolean after I don't have to deal with react's strict mode anymore const [reRender, setReRender] = useState(false); //only here as a state to change with useEffect useEffect(() => { const fetchData = async (data: any) => { try { const response = await fetch(data); const results = await response.json(); setProductList(results); const checked = new Array(results.length).fill(false); setCheckedItems(checked); } catch (e) { console.log(e); } }; fetchData(API); }, []); const handleOnChange = (position:number) => { const updatedCheckedItems = checkedItems.map((item, index) => index === position ? !item : item ); setCheckedItems(updatedCheckedItems); let skuList = updatedCheckedItems.map( (currentState, index) => { if (currentState === true) { return productList[index].sku; } } ); const cleanList = skuList.filter( Boolean ); cleanList.forEach(queryReady) function queryReady(item:any, index:number, arr:any) { arr[index] = `sku=${item}`; } setDeleteList(cleanList) }; useEffect(() => { if(renderBlock.current === 2) { api.delete(`/delete?${deleteList.join("&")}`).then((res) => { console.log(res.data) }).catch((error) => { console.log(error) }); setReRender(!reRender); //figured this should trigger a re-render... it doesn't. }; renderBlock.current = renderBlock.current + 1; //just the janky strict mode workaround }, [del]); return ( <div> {productList.length === 0 && <h4>Fetching Checkboxes ...</h4>} {productList.length > 0 && <Container> {productList.map((item: any, index) => ( <ProductContainer> <input type="checkbox" value={item.sku} id={`custom-checkbox-${index}`} checked={checkedItems[index]} onChange={() => handleOnChange(index)} /> <ItemDescription> <span className="sku" >{item.sku}</span> <span className="name" >{item.name}</span> <span className="price" >{item.price} $</span> {item.size && <span className="size" >Size: {item.size}MB</span>} {item.weight && <span className="weight" >Weight: {item.weight}KG</span>} {item.height && <span className="dimention" >Dimention: {item.height} x {item.width} x {item.length}</span>} </ItemDescription> </ProductContainer> ))} </Container> } </div> ); }; export default ProductDisplay;
父组件ProductList代码
import ListHeader from "../components/headers/list_header"; import ProductDisplay from "../components/product_display"; import Footer from "../components/footer"; import { Container } from "./styles"; import { useState } from "react"; function ProductList(): JSX.Element { const [del, setDel] = useState(false); return ( <Container> <ListHeader del={del} setDel={setDel} /> <ProductDisplay del={del} setDel={setDel} /> <Footer /> </Container> ) } export default ProductList;
头部组件ListHeader代码
import { AddButton, ButtonContainer, Container, DeleteButton, TitleContainer } from "./style"; import { Link } from "react-router-dom"; interface IProps { del: boolean setDel: (del: boolean) => void } function ListHeader({del, setDel}: IProps){ return ( <Container> <TitleContainer> <h1> Product List </h1> </TitleContainer> <ButtonContainer> <Link to='/add-product'> <AddButton> ADD </AddButton> </Link> <DeleteButton id="delete-product-btn" onClick={() => setDel(!del)}> MASS DELETE </DeleteButton> </ButtonContainer> </Container> ) } export default ListHeader;
我尝试过用setReRender或传入的setDel触发重渲染,但都没用。希望找到问题根源,不用页面刷新更新界面,同时搞懂背后的原理。
回答
问题根源
- 核心数据未更新:组件渲染依赖
productList状态,但删除操作完成后,你没有重新获取最新的商品列表并更新这个状态。setReRender(!reRender)确实能触发重渲染,但productList还是旧数据,所以界面不会变。 del状态逻辑混乱:点击删除按钮时del在true/false间切换,但你的删除逻辑只在renderBlock.current === 2时执行,后续点击del切换回false,不会触发列表刷新。- Strict Mode的临时方案干扰逻辑:
renderBlock的计数方式让删除逻辑的触发条件不可靠,而且没有和数据更新绑定。
解决方案
1. 修正Props类型
首先给ProductDisplay的IProps补上setDel,因为父组件传了这个属性:
interface IProps { del: boolean; setDel: (del: boolean) => void; }
2. 重构删除与列表刷新逻辑
把获取列表的逻辑抽成独立函数,删除成功后立即重新请求最新数据,更新productList:
// 抽离获取列表的函数,方便复用 const fetchProductList = async () => { try { const response = await fetch(API); const results = await response.json(); setProductList(results); const checked = new Array(results.length).fill(false); setCheckedItems(checked); } catch (e) { console.log(e); } }; // 初始加载列表 useEffect(() => { fetchProductList(); }, []); // 处理删除的useEffect useEffect(() => { // 只有del为true且有要删除的商品时执行 if (del && deleteList.length > 0) { api.delete(`/delete?${deleteList.join("&")}`) .then(() => { // 删除成功后重新获取最新列表 fetchProductList(); // 重置del状态,确保下次点击能触发 setDel(false); // 清空删除列表和选中状态 setDeleteList([]); }) .catch((error) => { console.log(error); // 失败也重置del状态 setDel(false); }); } }, [del, deleteList, setDel]);
3. 简化handleOnChange逻辑
优化选中商品的处理,去掉冗余的forEach:
const handleOnChange = (position: number) => { const updatedCheckedItems = checkedItems.map((item, index) => index === position ? !item : item ); setCheckedItems(updatedCheckedItems); // 直接生成带sku参数的删除列表 const formattedDeleteList = updatedCheckedItems .map((isChecked, index) => isChecked ? `sku=${productList[index].sku}` : null) .filter(Boolean); setDeleteList(formattedDeleteList); };
4. 移除无用状态
删掉renderBlock和reRender,这些都是不必要的,现在的逻辑已经能正确触发更新。
背后的原理
- React状态驱动渲染:React组件的渲染完全由状态(
useState)和Props的变化决定。只有当状态或Props更新时,组件才会重新渲染,并用新数据生成DOM。 useEffect的依赖机制:useEffect会在依赖数组中的值发生变化时执行。之前的代码没有更新productList,所以即使触发重渲染,界面还是旧数据。- 状态重置的必要性:删除完成后把
del重置为false,是为了让下次点击删除按钮时,del从false变为true,能再次触发useEffect执行。
内容的提问来源于stack exchange,提问作者Leon Murelli SIlveira
相关产品推荐
相关产品推荐

