React+Remix加载数据时如何避免无限循环?
问题描述
我开发了一个可添加任意数量图表组件的网页,每个组件包含选择框,供用户选择两种货币。当选择变更后,程序需从数据库获取所选两种货币的汇率数据。
- 在
_index.tsx中,从数据库获取货币列表并渲染选择框的功能正常。 - 在
chart.$type.$from.$to.tsx中遇到两个核心问题:- 若在
useEffect中用await调用fetcher.load并将effect设为async,会触发destroy is not a function错误; - 移除异步代码后,
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
相关产品推荐
相关产品推荐

