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

React使用useEffect自定义useFetch时组件为何两次渲染?

问题现象
  • 页面最终展示内容:Hello { "name" : "Jack" }
  • 控制台打印顺序如下:
    1. App rendering
    2. useFetch useEffect
    3. App rendering
相关代码

App.js

import { useFetch } from './useFetch';

function App() {
    const { data } = useFetch({ url : "jack.json" })
    console.log('App rendering')
    return (
        <div className="App">
        <div>Hello</div>
        <div>{JSON.stringify(data)}</div>
        </div>
    )
}

export default App;

useFetch.js

import { useState, useEffect } from "react";

export const useFetch = (options) => {
    const [data, setData] = useState(null);
    useEffect(() => {
        console.log("useFetch useEffect");
        fetch(options.url)
            .then( response => response.json())
            .then( json  => setData(json))
    }, [])

    return { data }
}
逐步骤底层执行流程拆解

整个执行逻辑完全遵循React的渲染规则,不存在“渲染速度太快看不到”的玄学,每一步都是可预测的:

  1. 首次挂载渲染阶段(同步执行)

    • React开始首次挂载App组件,同步调用App函数
    • 函数执行到useFetch调用时,同步进入自定义Hook逻辑:
      • 首先执行useState(null),初始化data状态为初始值null
      • 注册useEffect回调:useEffect属于副作用调度API,不会在渲染阶段同步执行,React只会把这个回调存入待执行的effect队列,等整个组件渲染提交到DOM之后才会运行
      • 同步执行到useFetch的return语句,返回{ data: null }
    • App拿到返回的data值为null,执行console.log打印第一条日志App rendering,生成首次渲染的虚拟DOM。这里要纠正之前的推测错误:首次渲染时data是null不是undefined,对应页面data区域会渲染null,如果本地接口响应速度极快,这个状态确实可能肉眼无法捕捉,但逻辑上是真实存在的
    • React把虚拟DOM转换为真实DOM挂载到页面,首次渲染流程结束
  2. 副作用执行阶段(渲染提交后异步非阻塞执行)

    • 首次DOM挂载完成后,React开始清空待执行的effect队列,执行useFetch里注册的useEffect回调:
      • 首先打印第二条日志useFetch useEffect
      • 发起fetch请求,fetch返回的是Promise对象,.then的回调会被推入微任务队列,不会阻塞主线程,useEffect回调本身执行完毕
  3. 状态更新触发重渲染阶段

    • 事件循环执行微任务队列里的fetch回调:先把响应解析为JSON格式的{ "name": "Jack" },接着调用setData(json)触发状态更新
    • React检测到useFetch内部的data状态变化,标记App组件需要重渲染
    • React重新调用App函数执行二次渲染:
      • 再次进入useFetch逻辑,此时useState返回的是更新后的data值{ "name": "Jack" }
      • 因为useEffect的依赖数组为空[],不会重复注册或执行该副作用
      • useFetch返回{ data: { "name": "Jack" } }
      • App执行console.log打印第三条日志App rendering,生成新的虚拟DOM
    • React对比新旧虚拟DOM的差异,更新真实DOM中data对应的区域,页面最终展示Hello { "name" : "Jack" }
核心疑问解答
  • 为什么App组件会触发2次渲染?

两次渲染的触发原因完全独立:第一次是组件首次挂载的初始化渲染,第二次是useFetch内部data状态从null更新为接口返回值触发的重渲染。React的重渲染规则只关心组件使用的状态是否发生更新,和状态更新的逻辑写在组件本体还是自定义Hook里没有关系。

  • 为什么useFetch不能等待内部fetch执行完成再返回值?

本质原因是React组件渲染和Hook执行都是同步的,没有能力暂停渲染流程等待异步逻辑完成:

  • 自定义Hook本质就是普通JavaScript函数,组件渲染时会同步执行Hook内的所有同步代码、拿到返回值后继续后续渲染流程,不可能为了等待异步请求阻塞整个渲染,否则会导致页面长时间白屏,交互无响应。
  • 如果要实现“等异步数据加载完再渲染组件内容”的效果,需要配合React的Suspense异步组件能力做上层封装,普通的useState+useEffect写法无法实现首次返回就拿到异步结果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 21:06:36