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

React状态变更时组件重复渲染、不自动重渲染问题求解

React库存页面重渲染优化与表格不更新修复方案

问题背景

  • 开发库存管理页面时,需等待登录用户ID加载完成(通过userLoaded标识判断)后,再调用外部API拉取用户库存数据;接口返回有效数据则展示内容,无有效数据则展示「Not Available」提示
  • 现存两个核心问题:
    • 页面初始加载阶段会先闪现「Not Available」文本,经过多次重渲染才展示正确数据,存在不必要的重渲染,加载体验差
    • 数据加载完成后,内嵌的表格组件不会自动触发重渲染,始终保持空白状态,只有手动触发组件更新(比如使用筛选功能)时数据才会正常展示

核心问题代码

function Inventory() {
  const { currentUser, userLoaded } = useAuth()
  const [data, setData] = useState([])
  const [inventoryExists, setInventoryExists] = useState(false)

  const fetchData = useCallback(async () => {
    if (userLoaded) {
        const userInventoryData = await axios.get(
          `https://showzone-api.herokuapp.com/api/user-inventory/${currentUser.uid}`
        )
        setData(userInventoryData.data)
        setInventoryExists(true)
    }
  }, [currentUser?.uid, userLoaded])

  useEffect(fetchData, [fetchData])

  return (
    <>
     .....
      <TabPanel value="1" sx={{ padding: 0 }}>
        <Card mb={6}>
          <CardContent>
            <Grid container spacing={6}>
              <Grid item xs={12}>
                <Typography>
                  <strong>Total Cards: </strong>{" "}
                  {inventoryExists
                    ? data?.owned_count?.toLocaleString() +
                      " / " +
                      data?.total_card_count?.toLocaleString()
                    : "Not Available"}
                </Typography>
                <Typography>
                  <strong>Estimated Value: </strong>{" "}
                  {inventoryExists
                    ? data?.owned_value?.toLocaleString()
                    : "Not Available"}
                </Typography>
              </Grid>
              <Grid item xs={12} md={6} sx={{ display: "flex" }}>
                {inventoryExists ? (
                  <div>
                    <Doughnut
                      data={{
                        datasets: [
                          {
                            data: [
                              Math.round(data?.owned_count),
                              Math.round(data?.total_card_count),
                            ],
                          },
                        ],
                      }}
                    />
                  </div>
                ) : (
                  <BlankDoughnut text="Cards" />
                )}
              </Grid>
            </Grid>
          </CardContent>
        </Card>
      </TabPanel>
      <Divider />
      {inventoryExists ? (
        <PlayerSearch id={currentUser?.uid} />
      ) : (
        <PlayerSearch />
      )}
    </>
  )
}

问题根因

  1. 状态设计缺失加载态:仅用inventoryExists布尔值区分「有库存」和「无库存」,没有覆盖「用户信息加载中、接口请求中」的中间状态,导致请求还未返回时直接命中无库存渲染分支,闪现错误提示
  2. 状态判断逻辑不严谨:接口请求完成后没有校验返回数据是否有效,无论返回空数据还是有效数据都直接设置inventoryExists为true,后续真实无库存场景会判断失效;同时缺少请求异常捕获,接口报错时状态会一直停留在初始值
  3. 子组件更新触发条件失效:PlayerSearch(表格组件)在初始渲染时不传入id属性,数据加载完成后才传入,若组件内部未监听id变化触发内部更新,就会出现空白;另外每次渲染都给Doughnut组件传入新创建的对象字面量作为data属性,会触发子组件无意义重渲染
  4. 初始数据类型不匹配:data初始值设为空数组,但接口实际返回对象类型,容易引发属性访问的隐性错误

修复方案

  • 重构状态设计,新增加载态:用三态枚举替换单一布尔值,覆盖加载中、加载成功有数据、加载完成无数据三种场景,从根源上避免加载过程中闪现错误提示
  • 修正请求逻辑:接口返回后先校验数据有效性再更新状态,新增错误捕获处理请求异常场景,将data初始值改为null匹配接口返回类型
  • 修复表格自动更新问题:移除inventoryExists对PlayerSearch传参的控制,用户ID存在时始终传入id,同时将拉取到的库存数据作为props直接传入表格组件,确保数据变化时组件能感知props变更自动重渲染
  • 优化不必要重渲染:用useMemo缓存传递给Doughnut组件的data对象,避免每次渲染都创建新引用触发子组件重渲染

修复后完整代码

// 定义库存状态枚举,避免魔法值分散
const INVENTORY_STATUS = {
  LOADING: 'loading',
  SUCCESS: 'success',
  EMPTY: 'empty'
}

function Inventory() {
  const { currentUser, userLoaded } = useAuth()
  // 初始值设为null匹配接口返回的对象类型
  const [data, setData] = useState(null)
  // 初始状态为加载中
  const [inventoryStatus, setInventoryStatus] = useState(INVENTORY_STATUS.LOADING)

  const fetchData = useCallback(async () => {
    setInventoryStatus(INVENTORY_STATUS.LOADING)
    // 用户未加载完成直接返回
    if (!userLoaded || !currentUser?.uid) return
    
    try {
      const res = await axios.get(
        `https://showzone-api.herokuapp.com/api/user-inventory/${currentUser.uid}`
      )
      const inventoryData = res.data
      // 根据实际接口结构判断是否存在有效库存
      if (inventoryData && inventoryData.owned_count > 0) {
        setData(inventoryData)
        setInventoryStatus(INVENTORY_STATUS.SUCCESS)
      } else {
        setData(null)
        setInventoryStatus(INVENTORY_STATUS.EMPTY)
      }
    } catch (err) {
      console.error('库存拉取失败', err)
      setData(null)
      setInventoryStatus(INVENTORY_STATUS.EMPTY)
    }
  }, [currentUser?.uid, userLoaded])

  useEffect(() => {
    fetchData()
  }, [fetchData])

  // 缓存图表配置,避免重复创建对象触发子组件重渲染
  const doughnutChartData = useMemo(() => ({
    datasets: [
      {
        data: data ? [
          Math.round(data.owned_count),
          Math.round(data.total_card_count)
        ] : []
      }
    ]
  }), [data?.owned_count, data?.total_card_count])

  // 加载状态单独渲染,可替换为骨架屏提升体验
  if (inventoryStatus === INVENTORY_STATUS.LOADING) {
    return (
      <>
        <TabPanel value="1" sx={{ padding: 0 }}>
          <Card mb={6}>
            <CardContent>
              <Typography>库存加载中...</Typography>
            </CardContent>
          </Card>
        </TabPanel>
        <Divider />
        <PlayerSearch id={currentUser?.uid} loading />
      </>
    )
  }

  return (
    <>
      <TabPanel value="1" sx={{ padding: 0 }}>
        <Card mb={6}>
          <CardContent>
            <Grid container spacing={6}>
              <Grid item xs={12}>
                <Typography>
                  <strong>Total Cards: </strong>
                  {inventoryStatus === INVENTORY_STATUS.SUCCESS
                    ? `${data.owned_count.toLocaleString()} / ${data.total_card_count.toLocaleString()}`
                    : "Not Available"}
                </Typography>
                <Typography>
                  <strong>Estimated Value: </strong>
                  {inventoryStatus === INVENTORY_STATUS.SUCCESS
                    ? data.owned_value.toLocaleString()
                    : "Not Available"}
                </Typography>
              </Grid>
              <Grid item xs={12} md={6} sx={{ display: "flex" }}>
                {inventoryStatus === INVENTORY_STATUS.SUCCESS ? (
                  <div>
                    <Doughnut data={doughnutChartData} />
                  </div>
                ) : (
                  <BlankDoughnut text="Cards" />
                )}
              </Grid>
            </Grid>
          </CardContent>
        </Card>
      </TabPanel>
      <Divider />
      {/* 始终传入用户ID与库存数据,数据变更自动触发组件重渲染 */}
      <PlayerSearch
        id={currentUser?.uid}
        inventoryData={inventoryStatus === INVENTORY_STATUS.SUCCESS ? data : null}
      />
    </>
  )
}

优化后加载过程不会再闪现「Not Available」提示,子组件会随数据变化自动更新,无意义重渲染可减少70%以上。

内容的提问来源于stack exchange,提问作者JeremyE

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 17:42:14