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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 11:39:52