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

React+Recharts:多子组件同步更新父组件State异常排查

问题描述

在React项目中使用Recharts图表库时,我需要获取X轴刻度的位置,以此绘制代表今日日期的参考线。我将父组件的state传递给自定义刻度组件CustomizedTick,尝试把每个刻度的X轴坐标值添加到父组件的bars state中,但最终bars仅包含一个刻度的值(比如{4: 352}),而预期应该是所有刻度索引与对应X坐标的映射(比如{0: 32, 1: 98, 2: 203, 3: 278, 4: 352})。

相关代码

CustomizedTick 组件

import React, { FC, useEffect } from 'react';

export const CustomizedTick: FC<any> = (props: any) => {
  const { x, y, payload, index, barStates, setBars } = props;

  useEffect(() => {
    setBars({ ...barStates, [index]: x - 60 });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [x]);
  const parts = payload.value.split(SPLIT_PARAM);

  return (
    <g transform={`translate(${x},${y})`}>
      <text style={{ fontSize: '10px' }} x={0} y={0} dy={16} fill="#666">
        <tspan textAnchor="middle" x="0">
          {parts[0]}
        </tspan>
        <tspan textAnchor="middle" x="0" dy="20">
          {parts[1].slice(4)}
        </tspan>
      </text>
    </g>
  );
};

父组件 Graph

import React, { FC, useEffect, useMemo, useState } from 'react';
import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  ResponsiveContainer,
  ReferenceLine,
  LabelList,
} from 'recharts';

const Graph = () => {
const splitData = [
    {
        "createdAt": "2022-08-12T09:39:31.796234+00:00",
        "creditorcode": "CRED1",
        "costamount": 10,
        "costinvoicenumber": "100001",
        "type": "cost",
        "updatedAt": "2022-08-12T09:39:31.796238+00:00",
        "displayDate": "2022-08-17T14:51:59.014Z"
    },
    {
        "createdAt": "2022-08-12T09:39:31.796234+00:00",
        "debtorcode": "DEBT1",
        "sellamount": 11.5,
        "sellcurrency": "USD",
        "sellinvoicedate": "2022-02-01 00:00:00",
        "type": "sell",
        "updatedAt": "2022-08-12T09:39:31.796238+00:00",
        "displayDate": "2022-08-17T14:51:59.014Z"
    },
    {
        "createdAt": "2022-08-12T09:46:38.379872+00:00",
        "debtorcode": "DEBT2",
        "sellamount": 57.5,
        "sellcurrency": "USD",
        "type": "sell",
        "updatedAt": "2022-08-12T09:46:38.379875+00:00",
        "displayDate": "2022-08-17T14:51:59.014Z"
    },
    {
        "createdAt": "2022-08-12T09:46:38.379872+00:00",
        "creditorcode": "CRED3",
        "costamount": 30,
        "costinvoicenumber": "100003",
        "type": "cost",
        "updatedAt": "2022-08-12T09:46:38.379875+00:00",
        "displayDate": "2022-08-17T14:51:59.014Z"
    }
]

const [bars, setBars] = useState<any>({});

 useEffect(() => {
    const dateNow: Date = new Date(Date.now());
    let dataItemIndex: number | string;
    for (dataItemIndex in splitData) {
      if (!bars[dataItemIndex] || bars[dataItemIndex] === 0) {
        break;
      }

      if (
        splitData[dataItemIndex].displayDate &&
        // @ts-ignore
        splitData[dataItemIndex].displayDate > dateNow
      ) {
        if (parseInt(dataItemIndex) === 0) {
          setCurrentDate(15);
          break;
        }
        const col2 = bars[dataItemIndex];
        const col1 = parseInt(dataItemIndex) > 0 ? bars[parseInt(dataItemIndex) - 1] : 0;
        setCurrentDate((col2 + col1) / 2);
        break;
      }
    }
  }, [bars, splitData]);
           return ( <BarChart
              width={width}
              height={height}
              data={data}
              margin={{
                top: 20,
                right: 20,
                left: 0,
                bottom: 20,
              }}
            >
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis
                dataKey="name"
                type="category"
                dy={16}
                tick={<CustomizedTick barStates={barStates} />}
                minTickGap={0}
                interval={0}
              />
              <YAxis type="number" domain={['auto', 'auto']} />
              <ReferenceLine
                y={0}
                label={
                  currentDate !== 0 ? (
                    <TimeLine
                      xoffset={currentDate}
                      yheight={1200}
                      stroke={'#0079FF'}
                      strokeWidth={2}
                      strokeOpacity={1}
                    />
                  ) : undefined
                }
                stroke={colors.black5}
              />
              <Bar dataKey="y">
                <LabelList
                  fill={colors.black3}
                  position="insideTop"
                  formatter={(v: number) => v.toFixed(2)}
                  offset={8}
                />
              </Bar>
            </BarChart> )
}

问题原因

  1. 异步更新导致的状态覆盖:CustomizedTick组件中使用{...barStates, [index]: x - 60}更新状态时,barStates是组件挂载时捕获的旧值,多个刻度的useEffect异步执行,后执行的更新会基于初始空对象覆盖前面的结果,最终只保留最后一个刻度的数据。
  2. 父组件传参错误:父组件给CustomizedTick传递的barStates={barStates}是未定义的变量,实际应该传递父组件的bars state,导致子组件拿到的始终是空对象。

解决方法

1. 使用函数式更新状态

修改CustomizedTick中的useEffect,利用setBars的函数式更新,确保每次更新都基于最新的状态:

useEffect(() => {
  setBars(prevBars => ({ ...prevBars, [index]: x - 60 }));
}, [x, index, setBars]);

同时移除eslint禁用注释,补全依赖项,避免闭包捕获旧值的问题。

2. 修正父组件传参

将父组件中XAxis的tick属性传参改为:

tick={<CustomizedTick barStates={bars} setBars={setBars} />}

确保子组件拿到的是父组件最新的bars状态。

3. 额外优化

  • 确保SPLIT_PARAM变量在CustomizedTick组件中已定义,避免运行时错误;
  • 可以根据数据长度初始化bars的初始值,减少不必要的状态更新。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 17:57:14