React Native多图表组件渲染失败问题排查求助
React Native 多图表渲染异常问题(超过2个图表时第3个及以后渲染错乱)
在React Native中使用API数据渲染一系列图表组件时,当渲染超过2个图表(每个图表平均包含1000-2000个数据点),第3个及之后的图表会出现渲染异常——图表网格线、组件头部和footer损坏,布局错乱。旋转屏幕触发重绘有时可修复,但首次渲染必现问题。数据本身可正常输出,无控制台报错,图表仅需一次性拉取数据,无需实时更新。
容器组件代码片段
<View className={'w-full'}> {chartWidgets && chartWidgets.length > 0 && chartWidgets?.map((w: PageWidget<IQuickGraphWidget>, i) => { return ( <View className={'w-full mb-4'} key={i}> {w?.widget?.settings?.graph?.split(':')[1] && ( <ChartWidget enabled={true} widgetData={w} key={i} onSelectControl={() => {}} pointData={{}} graphId={w?.widget?.settings?.graph?.split(':')[1]} pos={i} /> )} </View> ); })} </View>
ChartWidget组件代码
/** Vendor */ import React, {useEffect, useRef, useState} from 'react'; import { Pressable, View, Text, ActivityIndicator, TextInput, } from 'react-native'; import {useQueries, useQuery} from '@tanstack/react-query'; import Modal from 'react-native-modal'; import {RadioGroup} from 'react-native-radio-buttons-group'; import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'; import { faEllipsisVertical, faLineChart, } from '@fortawesome/free-solid-svg-icons'; import {useNavigation} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; /** Lib */ import { someApiEndpoint } from 'someApiEndpoint'; import {cardShadow} from '../../utils/nativeStyles'; /** State */ import {useStateContext} from '../../stateManager'; import {VictoryLineChart} from './VictoryLineChart'; import {VictoryBarChart} from './VictoryBarChart'; export const ChartWidget: React.FC<any> = ({ widgetData, pointData, handleOnSelectControl, enabled = true, graphId = '', inView = false, triggerModal = false, pos = 0, }) => { /** Variables */ const navigation = useNavigation<StackNavigationProp<any>>(); const {appState} = useStateContext(); const defaultEndDate = new Date(); const defaultStartDate = new Date(); defaultStartDate.setDate(defaultEndDate.getDate() - 1); const [startDate, setStartDate] = useState<Date | null>(null); const [endDate, setEndDate] = useState(defaultEndDate); const timeRangeOptions: any[] = [ {id: '1', label: '1 Hour', value: 1}, {id: '2', label: '12 Hours', value: 12}, {id: '3', label: '1 Day', value: 24}, {id: '4', label: '1 Week', value: 168}, {id: '5', label: '1 Month', value: 720}, {id: '6', label: '1 Year', value: 8760}, ]; const [selectedTimeRangeOption, setSelectedTimeRangeOption] = useState('3'); const [scaleMinValue, setScaleMinValue] = useState(''); const [scaleMaxValue, setScaleMaxValue] = useState(''); const [showModal, setShowModal] = useState(false); const [modalSettingType, setModalSettingType] = useState(''); const [initDateRange, setInitDateRange] = useState(false); const [chartSettings, setChartSettings] = useState<any | null>(null); const [chartData, setChartData] = useState<any | null>(null); const [isVisible, setIsVisible] = useState(enabled); const [shouldRefetch, setShouldRefetch] = useState(false); const [isLoading, setIsLoading] = useState(true); /** Utils */ const parseDate = (dt): string => { const padL = (nr, len = 2, chr = '0') => `${nr}`.padStart(2, chr); const returnString = `${padL(dt.getFullYear())}-${padL( dt.getMonth() + 1, )}-${dt.getDate()} ${padL(dt.getHours())}:${padL(dt.getMinutes())}:${padL( dt.getSeconds(), )}`; return returnString.trim(); }; const getSelectedRangeLabel = () => { const option = timeRangeOptions.find(i => i.id === selectedTimeRangeOption); return option?.label || ''; }; const setDefaultStartDateRangeBySetting = defaultTimePeriod => { const defaultStartfromSettings = new Date(); if (!startDate) { if (defaultTimePeriod === 604800) { setSelectedTimeRangeOption('4'); defaultStartfromSettings.setDate(defaultEndDate.getDate() - 7); } else { defaultStartfromSettings.setDate(defaultEndDate.getDate() - 1); } setStartDate(defaultStartfromSettings); setInitDateRange(true); return defaultStartfromSettings; } setInitDateRange(true); return startDate; }; const checkDisableForward = (): boolean => { const now = new Date(); now.setMinutes(0); now.setSeconds(0); now.setMilliseconds(0); return endDate > now; }; const getSettings = async (gId: number) => { setIsLoading(true); const settings = await someApiEndpoint( appState?.siteId, gId, ); setChartSettings(settings); getData(graphId, settings); }; const getData = async (gId, settings: any | null = null) => { setIsLoading(true); let queryStartDate: Date | null = startDate; if (isNaN(parseInt(gId)) || !settings?.web_quick_graph) { return null; } if (settings?.web_quick_graph && !queryStartDate) { queryStartDate = setDefaultStartDateRangeBySetting( settings.web_quick_graph?.settings?.default_time_period, ); } const res: any = await someApiEndpoint(); setChartData(res); }; /** Hooks */ useEffect(() => { if (graphId) { getSettings(graphId); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [graphId]); useEffect(() => { if (initDateRange) { getData(graphId, chartSettings); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [startDate, endDate]); // refetch when a user preference is changed useEffect(() => { if (shouldRefetch) { setShouldRefetch(false); getSettings(graphId); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldRefetch]); useEffect(() => { if (showModal) { return; } if (chartSettings) { const listSetting = chartSettings?.web_quick_graph?.settings?.auto_display || false; const visible = inView ? true : listSetting; setIsVisible(visible); if (!visible) { return; } } if (!!chartSettings && !!chartData) { if (chartSettings?.web_quick_graph?.user_scale) { setScaleMinValue( chartSettings?.web_quick_graph?.user_scale?.minimum.toString(), ); setScaleMaxValue( chartSettings?.web_quick_graph?.user_scale?.maximum.toString(), ); } else if (chartSettings?.web_quick_graph?.value_axes) { setScaleMinValue( chartSettings?.web_quick_graph?.value_axes[0].minimum.toString(), ); setScaleMaxValue( chartSettings?.web_quick_graph?.value_axes[0].maximum.toString(), ); } else { let largest = -Infinity; let smallest = Infinity; for (const item of chartData?.data) { if (Array.isArray(item?.g) && item?.g?.length > 0) { for (const value of item.g) { if (value > largest) { largest = value; } if (value < smallest) { smallest = value; } } } } setScaleMinValue(smallest.toString()); setScaleMaxValue(largest.toString()); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [chartSettings, chartData]); /** Handlers */ const handleOnCancel = () => {}; const handleOnOpenModal = () => {}; const handleOnSelectOption = val => {}; const handleOnSetScale = () => {}; const handleOnTimeline = () => {}; const handleOnBack = () => {}; const handleOnForward = () => {}; const handleOnNavigateToChart = () => {}; if (!isVisible || !chartSettings || !chartData) { return <></>; } return ( <View className="w-full flex flex-1 bg-white" style={!inView ? cardShadow : {}}> {!inView && ( <View className="flex flex-row justify-between w-full border-b border-ricado-gray mb-2 p-4"> <Text className="text-ricado-green text-sm text-left pt-1"> {widgetData?.widget?.settings?.title} </Text> <View className="flex flex-row"> <Pressable onPress={handleOnNavigateToChart} className="mt-2 mr-4"> <Text> <FontAwesomeIcon icon={faLineChart} /> </Text> </Pressable> <Pressable onPress={handleOnOpenModal} className="mt-2"> <Text> <FontAwesomeIcon icon={faEllipsisVertical} /> </Text> </Pressable> </View> </View> )} <View className={'w-full relative'} style={{minHeight: 300}}> {chartData && chartSettings?.web_quick_graph?.type === 'line' && ( <VictoryLineChart chartData={chartData} chartSettings={chartSettings} /> )} {chartData && chartSettings?.web_quick_graph?.type === 'bar' && ( <VictoryBarChart chartData={chartData} chartSettings={chartSettings} /> )} </View> <View className="flex flex-row justify-between w-full border-y border-ricado-gray mt-2 p-4"> <Pressable onPress={handleOnBack}> <Text className="text-black uppercase text-sm"> {'\u25C0'} Back {getSelectedRangeLabel()} </Text> </Pressable> <Pressable onPress={handleOnForward} disabled={checkDisableForward()}> <Text className={`${ checkDisableForward() ? 'text-gray-400' : 'text-black' } text-sm uppercase`}> Forward {getSelectedRangeLabel()} {'\u25B6'} </Text> </Pressable> </View> </View> ); };
已尝试的解决方案
- 更换多个图表库:Victory Native、Victory XL、AMCharts5、ECharts
- 使用React Hooks优化:
useMemo、useRef、useCallback - 分批渲染组件
- 减少单图表数据点数量
以上方案均未解决问题,仅前2个图表能正常渲染。
现寻求该问题的根本原因及可行的解决方案。
内容的提问来源于stack exchange,提问作者user21122267
相关产品推荐
相关产品推荐

