如何在useEffect加载数据时展示加载过程详情?
问题描述
自定义React组件首次挂载时,通过useEffect启动多步骤耗时数据加载流程,完成后再渲染核心内容。该组件并非总会渲染,且流程依赖特殊库调用,无法使用Axios、React Query等标准缓存库。
已配备进度展示组件及更新进度状态的函数,这些函数传入长流程,在步骤完成时定期调用,用于区分流程耗时与程序挂起,但存在以下问题:
- 进度状态更新被React自动批量处理,无法实时反馈进度
- 使用
flushSync包裹状态更新会触发警告:flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering.,且状态仍无法即时更新
需要一个规范可行的方案,实现在一个组件的useEffect(对应类组件的componentDidMount)钩子运行时,另一个组件的状态能多次更新并实时重新渲染。
复现代码
import React, { useEffect, useState } from 'react'; import { flushSync } from 'react-dom'; import ReactDOM from 'react-dom/client'; import { Line } from 'rc-progress'; //Progress bar functionality: normally a separate component imported in here interface ProgressProps { currentStepDescription: string, percent: number, allDone: boolean, } const Progress = function(props: ProgressProps) { console.log('Rendering progress; allDone is', props.allDone); return (props.allDone ? <span>All done!</span> : <> <span>{props.currentStepDescription}</span> <Line percent = {props.percent}/> </>); } //Loading script: Again, normally separate and imported const wait = (ms : number) => new Promise((r, j)=>setTimeout(r, ms)); const slowDataLoad = async function( start: number, resetProgressStep: ((currentStepDescription: string) => void), completeProgressStep: (() => void), abortCalled: (() => boolean), ) : Promise<number[] | undefined> { let result : number[] = []; if(abortCalled()) { return; } resetProgressStep('First part'); for(let i=start; i<10; i++) { if(abortCalled()) { return; } await wait(1000); //Simulates library call fetching from an exotic API result.push(i); completeProgressStep(); } resetProgressStep('Second part'); for(let i=start+10; i<20; i++) { if(abortCalled()) { return; } await wait(1000); //Simulates calls based on 1st results result.push(i); completeProgressStep(); } return result; } const PrimaryContent = function(props: {start: number}) { const [dataToShow, setDataToShow] = useState<number[]>([]); const [progressProps, setProgressProps] = useState<ProgressProps>(function(){ console.log('Re-initiating progress state.'); return {currentStepDescription: 'Loading', percent: 100, allDone: true}; }); const resetProgressStep = function(currentStepDescription: string) { flushSync(() => { console.log( 'Now starting ' + currentStepDescription + ', incl. setting allDone to false.' ); setProgressProps({currentStepDescription, percent: 0, allDone: false}); })}; const completeProgressStep = function() {flushSync(() => { console.log('Making progress. allDone is', progressProps.allDone); setProgressProps(Object.assign({}, progressProps, { percent: progressProps.percent + 10 })); })}; const finishProgress = function() {flushSync(() => { console.log('Setting allDone to true.'); //only happens once! setProgressProps(Object.assign({}, progressProps, {allDone: true})); })}; const getAndDisplayData = async function( start: number, abortCalled: (() => boolean), ) { const dataToShow = await slowDataLoad( start, resetProgressStep, completeProgressStep, abortCalled ); if(typeof dataToShow !== 'undefined') { if(abortCalled()) { return; } setDataToShow(dataToShow); finishProgress(); } }; useEffect(() => { //See beta.reactjs.org/learn/synchronizing-with-effects#fetching-data let isAborted = false; const abortCalled = function() {return isAborted}; getAndDisplayData(props.start, abortCalled); return() => { isAborted = true; console.log('Aborted first of 2 duplicated useEffect calls.') }; },[]); //https://stackoverflow.com/a/71434389/798371 return (<> <Progress {...progressProps} /> <br/> <span>{dataToShow.join(', ')}</span> </>); //actual display fn is way more complex } const App = function() { const start = 0; //normally parsed from URL & made more safe etc. return (start === undefined) ? (<span>'Displaying empty-state page here.'</span>) : (<PrimaryContent start={start}/>); } const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(<React.StrictMode><App /></React.StrictMode>);
可行解决方案
核心思路
移除干扰React渲染生命周期的flushSync,利用React在异步操作后自动处理状态更新并触发渲染的特性,同时改用函数式状态更新避免闭包导致的状态滞后问题。
具体修改步骤
- 删除
flushSync包裹:直接去掉所有flushSync调用,因为在await后的异步代码块中,React会自动处理状态更新并触发组件重新渲染。 - 改用函数式状态更新:所有更新
progressProps的操作,通过函数式更新获取最新状态值,确保进度计算准确。 - 保留取消逻辑:原有
isAborted的卸载取消逻辑无需改动,保证组件卸载时能终止加载流程。
修改后的关键代码
// 重置进度步骤 const resetProgressStep = function(currentStepDescription: string) { console.log( 'Now starting ' + currentStepDescription + ', incl. setting allDone to false.' ); setProgressProps({currentStepDescription, percent: 0, allDone: false}); }; // 更新进度 const completeProgressStep = function() { console.log('Making progress.'); setProgressProps(prev => ({ ...prev, percent: prev.percent + 10 })); }; // 完成进度 const finishProgress = function() { console.log('Setting allDone to true.'); setProgressProps(prev => ({ ...prev, allDone: true })); };
原理说明
- React在异步操作(如
await后的代码)中执行状态更新时,不会进行批量延迟处理,每次状态变更都会立即触发Progress组件的重新渲染,实现实时进度反馈。 - 函数式状态更新通过接收上一次的状态值作为参数,避免了闭包捕获旧状态导致的进度计算错误,确保每次更新都基于最新的状态。
内容的提问来源于stack exchange,提问作者WBT
相关产品推荐
相关产品推荐

