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

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触发重渲染,但都没用。希望找到问题根源,不用页面刷新更新界面,同时搞懂背后的原理。


回答

问题根源

  1. 核心数据未更新:组件渲染依赖productList状态,但删除操作完成后,你没有重新获取最新的商品列表并更新这个状态。setReRender(!reRender)确实能触发重渲染,但productList还是旧数据,所以界面不会变。
  2. del状态逻辑混乱:点击删除按钮时del在true/false间切换,但你的删除逻辑只在renderBlock.current === 2时执行,后续点击del切换回false,不会触发列表刷新。
  3. 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,这些都是不必要的,现在的逻辑已经能正确触发更新。

背后的原理

  1. React状态驱动渲染:React组件的渲染完全由状态(useState)和Props的变化决定。只有当状态或Props更新时,组件才会重新渲染,并用新数据生成DOM。
  2. useEffect的依赖机制:useEffect会在依赖数组中的值发生变化时执行。之前的代码没有更新productList,所以即使触发重渲染,界面还是旧数据。
  3. 状态重置的必要性:删除完成后把del重置为false,是为了让下次点击删除按钮时,del从false变为true,能再次触发useEffect执行。

内容的提问来源于stack exchange,提问作者Leon Murelli SIlveira

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 11:07:16