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> ) }
问题原因
- 异步更新导致的状态覆盖:
CustomizedTick组件中使用{...barStates, [index]: x - 60}更新状态时,barStates是组件挂载时捕获的旧值,多个刻度的useEffect异步执行,后执行的更新会基于初始空对象覆盖前面的结果,最终只保留最后一个刻度的数据。 - 父组件传参错误:父组件给
CustomizedTick传递的barStates={barStates}是未定义的变量,实际应该传递父组件的barsstate,导致子组件拿到的始终是空对象。
解决方法
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
相关产品推荐
相关产品推荐

