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

如何在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在异步操作后自动处理状态更新并触发渲染的特性,同时改用函数式状态更新避免闭包导致的状态滞后问题。

具体修改步骤

  1. 删除flushSync包裹:直接去掉所有flushSync调用,因为在await后的异步代码块中,React会自动处理状态更新并触发组件重新渲染。
  2. 改用函数式状态更新:所有更新progressProps的操作,通过函数式更新获取最新状态值,确保进度计算准确。
  3. 保留取消逻辑:原有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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 07:58:10