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

React+Remix加载数据时如何避免无限循环?

问题描述

我开发了一个可添加任意数量图表组件的网页,每个组件包含选择框,供用户选择两种货币。当选择变更后,程序需从数据库获取所选两种货币的汇率数据。

  • 在_index.tsx中,从数据库获取货币列表并渲染选择框的功能正常。
  • 在chart.$type.$from.$to.tsx中遇到两个核心问题:
    1. 若在useEffect中用await调用fetcher.load并将effect设为async,会触发destroy is not a function错误;
    2. 移除异步代码后,fetcher.data始终为undefined,无法获取数据。
  • 作为React和Remix新手,之前代码曾陷入无限循环,当前写法是为了避免循环,但不确定是否符合规范,也找不到真实异步取数的场景示例,想知道React/Remix开发者的正确解决思路和设计方案。

_index.tsx

import { db_client } from "./chartdata.server.tsx";
import { MyChart } from './chart.$type.$from.$to.tsx';

export async function loader({ request }: LoaderFunctionArgs) {
    const assets = await db_client.getAssets ();
    const response = json(assets);
    return response;
}

export default function Index() {
    const [charts, setCharts] = useState ([]);
    const [chartIdNum, setChartIdNum] = useState (1);

    const remChart = (deleteId) =>
    {
        setCharts ((charts) =>
        {
            for (let i = 0; i < charts.length; i++)
            {
                if (charts [i] === deleteId)
                    return charts.toSpliced (i, 1);
            }
        });
    };

    const addChart = () => {
        setCharts ((charts) => { return charts.concat (chartIdNum); });
        setChartIdNum ((chartIdNum) => { return chartIdNum + 1; });
    };

    return (
        <div>
            <div>
                <button onClick={addChart}>Add Chart</button>
            </div>

            <div>
                {charts.map ((id) => { return (<MyChart key={id} id={id} remove={remChart} />); })}
            </div>
        </div>
    );
}

chart.$type.$from.$to.tsx

import { db_client } from "./chartdata.server.tsx";
import { Chart } from 'react-google-charts';

export async function loader({ params }: LoaderFunctionArgs) {
    const prices = await db_client.getPrices (params.to, params.from);
    return json (prices);
}

export function MyChart ({id, remove})
{
    const fetcher = useFetcher();
    const [fromSelection, setFromSelection] = useState ('');
    const [toSelection, setToSelection] = useState ('');
    const [data, setData] = useState ([]);

    useEffect (
        async () =>
        {
            if (fromSelection.length > 0 && toSelection.length > 0)
            {
                if (data.length > 0)
                    setData ([]);

                let new_data = [["x", "low", "open", "close", "high"]];

                await fetcher.load ('/chart/' + 'candlestick' + '/' + fromSelection + '/' + toSelection);
//              for (let i = 0; i < fetcher.data.length; i++)
//                  new_data.push ([fetcher.data [i].period_begin, fetcher.data [i].low, fetcher.data [i].open, fetcher.data [i].close, fetcher.data [i].high]);

                if (JSON.stringify (data) !== JSON.stringify (new_data))
                    setData (new_data);
            }

        }, [fromSelection, toSelection, data]);

    async function handleCurrencySelected (e, curFrom)
    {
        if (curFrom && fromSelection != e.target.value)
            setFromSelection (e.target.value);
        else if (toSelection != e.target.value)
            setToSelection (e.target.value);
    }

    const options = { ... }; // omitting this because it is not causing the problem

    return (
        <div className="chart">

            <div>
                <button onClick={() => {remove (id);}}>Remove Chart</button>

                From:
                <select value={fromSelection} onChange={(e) => { handleCurrencySelected (e, true); }}>
                    <option key="empty" value=""></option>
                    {Object.keys (chartData).map ((k) => { return (<option key={k} value={k} disabled={toSelection !== null && toSelection == k}>{chartData [k]}</option>); })}
                </select>

                To:
                <select value={toSelection} onChange={(e) => { handleCurrencySelected (e, false); }}>
                    <option key="empty" value=""></option>
                    {Object.keys (chartData).map ((k) => { return (<option key={k} value={k} disabled={fromSelection !== null && fromSelection == k}>{chartData [k]}</option>); })}
                </select>
            </div>

            <div>chart id: { id }</div>

            <div>
                <Chart
                    chartType="CandlestickChart"
                    width="100%"
                    height="350px"
                    data={data}
                    options={options}
                />
            </div>
        </div>
    );
}

解决方案

1. 修复useEffect异步错误

React的useEffect回调不能是异步函数——它需要返回清理函数(或undefined),而async函数返回的Promise会被React当成清理函数调用,直接导致destroy is not a function错误。正确写法是在effect内部定义异步函数并执行:

useEffect(() => {
  const fetchData = async () => {
    if (fromSelection && toSelection) {
      await fetcher.load(`/chart/candlestick/${fromSelection}/${toSelection}`);
    }
  };

  fetchData();
}, [fromSelection, toSelection, fetcher]);

2. 正确获取fetcher.data

Remix的useFetcher会自动跟踪请求状态,fetcher.data会在请求完成后自动更新,不需要在await fetcher.load后立即读取。你需要单独监听fetcher.data的变化来更新图表数据:

useEffect(() => {
  if (fetcher.data && fromSelection && toSelection) {
    const newData = [["x", "low", "open", "close", "high"]];
    for (const item of fetcher.data) {
      newData.push([item.period_begin, item.low, item.open, item.close, item.high]);
    }
    setData(newData);
  }
}, [fetcher.data, fromSelection, toSelection]);

3. 彻底解决无限循环

之前的代码把data放进了effect依赖数组,而effect里又调用setData,这必然触发无限循环。解决方法是移除data作为依赖——图表数据完全由fetcher.data和选择状态决定,不需要把自身放进依赖。

4. 优化组件设计(可选)

  • 把货币列表通过props从父组件传递给MyChart:父组件的loader已经拿到了assets,直接传下去即可,避免子组件重复获取。
  • 给fetcher.load添加防抖:避免用户快速切换选择时发起大量请求,可以用第三方库use-debounce或自己实现简单防抖:
// 组件内添加防抖逻辑
const [debouncedFrom] = useDebounce(fromSelection, 300);
const [debouncedTo] = useDebounce(toSelection, 300);

useEffect(() => {
  const fetchData = async () => {
    if (debouncedFrom && debouncedTo) {
      await fetcher.load(`/chart/candlestick/${debouncedFrom}/${debouncedTo}`);
    }
  };

  fetchData();
}, [debouncedFrom, debouncedTo, fetcher]);

完整修正后的MyChart核心代码

export function MyChart({ id, remove, chartData }) { // 新增chartData props
  const fetcher = useFetcher();
  const [fromSelection, setFromSelection] = useState('');
  const [toSelection, setToSelection] = useState('');
  const [data, setData] = useState([["x", "low", "open", "close", "high"]]);

  // 发起请求
  useEffect(() => {
    const fetchData = async () => {
      if (fromSelection && toSelection) {
        await fetcher.load(`/chart/candlestick/${fromSelection}/${toSelection}`);
      }
    };

    fetchData();
  }, [fromSelection, toSelection, fetcher]);

  // 更新图表数据
  useEffect(() => {
    if (fetcher.data) {
      const newData = [["x", "low", "open", "close", "high"]];
      for (const item of fetcher.data) {
        newData.push([item.period_begin, item.low, item.open, item.close, item.high]);
      }
      setData(newData);
    }
  }, [fetcher.data]);

  // 处理选择逻辑(优化:避免选相同货币)
  function handleCurrencySelected(e, curFrom) {
    const value = e.target.value;
    if (curFrom) {
      if (value !== toSelection) {
        setFromSelection(value);
      }
    } else {
      if (value !== fromSelection) {
        setToSelection(value);
      }
    }
  }

  // 渲染部分保持不变
}

父组件Index需要传递chartData:

// Index组件内
const assets = useLoaderData(); // 获取父组件loader返回的货币列表

// 渲染MyChart时
<MyChart key={id} id={id} remove={remChart} chartData={assets} />

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 04:37:37