React Suspense 用法详解

React Suspense 用法详解

一、Suspense 是什么

Suspense 用于在子组件加载完成之前显示备用内容,例如:

  • 加载动画
  • 骨架屏
  • 加载提示
  • 懒加载组件
  • Suspense 数据请求

基本写法:

jsx 复制代码
import { Suspense } from 'react'

function App() {
  return (
    <Suspense fallback={<p>加载中...</p>}>
      <UserInfo />
    </Suspense>
  )
}

UserInfo 还没有准备好时,React 会显示:

jsx 复制代码
<p>加载中...</p>

准备完成后,React 会自动显示 UserInfo

官方文档:React Suspense


二、Suspense 的基本语法

jsx 复制代码
<Suspense fallback={备用内容}>
  {需要等待的组件}
</Suspense>

例如:

jsx 复制代码
<Suspense fallback={<Loading />}>
  <Profile />
</Suspense>

其中:

  • children:真正要显示的内容
  • fallback:加载期间显示的内容

fallback 可以是任意 React 节点:

jsx 复制代码
<Suspense fallback={<div>加载中...</div>}>
  <App />
</Suspense>
jsx 复制代码
<Suspense fallback={<Loading />}>
  <App />
</Suspense>
jsx 复制代码
<Suspense fallback={null}>
  <App />
</Suspense>

三、Suspense 配合 lazy 实现组件懒加载

这是 Suspense 最常见的用法。

1. 创建懒加载组件

jsx 复制代码
import { lazy, Suspense } from 'react'

const About = lazy(() => import('./About.jsx'))

function App() {
  return (
    <Suspense fallback={<p>About 加载中...</p>}>
      <About />
    </Suspense>
  )
}

export default App

执行过程:

text 复制代码
第一次渲染 About
      ↓
浏览器开始加载 About.js
      ↓
显示 fallback
      ↓
About.js 加载完成
      ↓
显示 About 组件

lazy 会延迟加载组件代码,直到组件第一次真正被渲染。React lazy 官方文档

2. 被懒加载的组件必须使用默认导出

About.jsx

jsx 复制代码
export default function About() {
  return <h1>关于我们</h1>
}

如果使用普通导出:

jsx 复制代码
export function About() {
  return <h1>关于我们</h1>
}

需要这样写:

jsx 复制代码
const About = lazy(() =>
  import('./About.jsx').then(module => ({
    default: module.About
  }))
)

3. lazy 必须写在组件外部

错误写法:

jsx 复制代码
function App() {
  const About = lazy(() => import('./About.jsx'))

  return (
    <Suspense fallback={<p>加载中...</p>}>
      <About />
    </Suspense>
  )
}

这样可能导致组件每次重新渲染时都被重新创建,内部状态被重置。

正确写法:

jsx 复制代码
const About = lazy(() => import('./About.jsx'))

function App() {
  return (
    <Suspense fallback={<p>加载中...</p>}>
      <About />
    </Suspense>
  )
}

四、Suspense 配合路由实现懒加载

使用 React Router 时,可以对页面进行懒加载:

jsx 复制代码
import { lazy, Suspense } from 'react'

const Home = lazy(() => import('./pages/Home.jsx'))
const User = lazy(() => import('./pages/User.jsx'))

function App() {
  return (
    <Suspense fallback={<p>页面加载中...</p>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/user" element={<User />} />
      </Routes>
    </Suspense>
  )
}

这样首页和用户页的代码不会一次性全部加载,而是在访问对应路由时再加载。

也可以针对不同页面设置不同的加载提示:

jsx 复制代码
<Route
  path="/user"
  element={
    <Suspense fallback={<p>用户页面加载中...</p>}>
      <User />
    </Suspense>
  }
/>

五、嵌套 Suspense

多个组件可以使用多个 Suspense 边界:

jsx 复制代码
function App() {
  return (
    <Suspense fallback={<p>页面加载中...</p>}>
      <Profile />

      <Suspense fallback={<p>文章加载中...</p>}>
        <ArticleList />
      </Suspense>
    </Suspense>
  )
}

加载过程:

text 复制代码
Profile 未加载完成
    ↓
显示"页面加载中"

Profile 加载完成
    ↓
显示 Profile
    ↓
ArticleList 未加载完成
    ↓
显示"文章加载中"

ArticleList 加载完成
    ↓
显示文章列表

嵌套 Suspense 可以让页面逐步展示,而不是所有内容加载完成后一次性显示。


六、一个 Suspense 边界会影响哪些内容

例如:

jsx 复制代码
<Suspense fallback={<Loading />}>
  <Header />
  <UserInfo />
  <ArticleList />
</Suspense>

如果 UserInfo 正在加载,整个 Suspense 区域都可能显示:

jsx 复制代码
<Loading />

也就是说,HeaderUserInfoArticleList 会被当成一个整体处理。

如果只希望文章区域显示加载状态,可以缩小边界:

jsx 复制代码
<Header />

<Suspense fallback={<p>用户信息加载中...</p>}>
  <UserInfo />
</Suspense>

<Suspense fallback={<p>文章加载中...</p>}>
  <ArticleList />
</Suspense>

Suspense 边界不应该随意包裹每一个小组件,而应该根据页面的加载体验来划分。


七、Suspense 不能直接监听 useEffect 中的请求

下面这种写法不会触发 Suspense:

jsx 复制代码
function UserList() {
  const [loading, setLoading] = useState(true)
  const [users, setUsers] = useState([])

  useEffect(() => {
    fetch('/api/users')
      .then(response => response.json())
      .then(data => {
        setUsers(data)
        setLoading(false)
      })
  }, [])

  return <div>用户列表</div>
}

即使外层有:

jsx 复制代码
<Suspense fallback={<p>加载中...</p>}>
  <UserList />
</Suspense>

也不会自动显示 Suspense 的 fallback

原因是:

Suspense 不会自动检测 useEffect 中发起的请求。

官方文档也明确说明,普通 useEffect 中的数据请求不会激活 Suspense。React Suspense 官方说明

普通项目中可以继续使用:

jsx 复制代码
if (loading) {
  return <p>加载中...</p>
}

如果要让数据请求触发 Suspense,需要使用:

  • 支持 Suspense 的框架
  • 支持 Suspense 的数据请求库
  • React 19 的 use(Promise)
  • 自己实现 Promise 缓存

八、React 19 中使用 use 和 Suspense 加载数据

React 19 提供了 use API,可以读取 Promise。

jsx 复制代码
import { Suspense, use } from 'react'

function UserInfo({ userPromise }) {
  const user = use(userPromise)

  return (
    <div>
      用户名:{user.name}
    </div>
  )
}

function App() {
  const userPromise = fetch('/api/user')
    .then(response => response.json())

  return (
    <Suspense fallback={<p>用户信息加载中...</p>}>
      <UserInfo userPromise={userPromise} />
    </Suspense>
  )
}

export default App

当 Promise 处于 pending 状态时,Suspense 会显示 fallback

Promise 完成后,Suspense 会显示真正的内容。

use 读取 Promise 时,必须保证 Promise 可以复用,不能在每次渲染时都创建一个全新的 Promise。React use 官方文档

不推荐的写法

jsx 复制代码
function UserInfo() {
  const user = use(
    fetch('/api/user').then(response => response.json())
  )

  return <div>{user.name}</div>
}

每次渲染都可能重新创建 Promise,导致重复请求或持续显示 fallback。

可以使用简单的缓存:

jsx 复制代码
const cache = new Map()

function fetchUser(url) {
  if (!cache.has(url)) {
    const promise = fetch(url).then(response => {
      if (!response.ok) {
        throw new Error('请求失败')
      }

      return response.json()
    })

    cache.set(url, promise)
  }

  return cache.get(url)
}

使用:

jsx 复制代码
function UserInfo() {
  const user = use(fetchUser('/api/user'))

  return <div>用户名:{user.name}</div>
}

实际开发中,更推荐使用框架或数据请求库提供的缓存方案,而不是自己实现完整的数据缓存。


九、Suspense 配合错误边界

Suspense 主要处理"等待加载",不负责处理请求错误。

如果 Promise 被拒绝,需要使用 Error Boundary 处理错误。

jsx 复制代码
import { Component, Suspense } from 'react'

class ErrorBoundary extends Component {
  constructor(props) {
    super(props)

    this.state = {
      hasError: false
    }
  }

  static getDerivedStateFromError() {
    return {
      hasError: true
    }
  }

  render() {
    if (this.state.hasError) {
      return <p>加载失败,请稍后重试</p>
    }

    return this.props.children
  }
}

使用:

jsx 复制代码
<ErrorBoundary>
  <Suspense fallback={<p>加载中...</p>}>
    <UserInfo />
  </Suspense>
</ErrorBoundary>

职责可以这样理解:

text 复制代码
Suspense       处理等待
ErrorBoundary  处理错误

十、更新时避免页面闪回 Loading

当已经显示的内容因为更新再次触发 Suspense 时,页面可能重新显示 fallback:

text 复制代码
旧页面
  ↓
切换新页面
  ↓
新页面加载中
  ↓
旧页面被替换成 Loading

这种体验可能比较突兀。

可以使用 startTransition

jsx 复制代码
import {
  Suspense,
  startTransition,
  useState
} from 'react'

function App() {
  const [page, setPage] = useState('home')

  function handleChangePage(nextPage) {
    startTransition(() => {
      setPage(nextPage)
    })
  }

  return (
    <div>
      <button onClick={() => handleChangePage('home')}>
        首页
      </button>

      <button onClick={() => handleChangePage('user')}>
        用户页
      </button>

      <Suspense fallback={<p>页面加载中...</p>}>
        {page === 'home' ? <Home /> : <User />}
      </Suspense>
    </div>
  )
}

startTransition 会告诉 React:

这是一个不紧急的更新,可以尽量保留当前已经显示的内容。

React 官方也建议使用 startTransition 避免更新时已经展示的内容突然被 fallback 替换。React useTransition 文档


十一、Suspense 和普通 loading 的区别

普通 loading

jsx 复制代码
function App() {
  const [loading, setLoading] = useState(true)

  if (loading) {
    return <Loading />
  }

  return <Content />
}

特点:

  • 手动管理 loading
  • 手动管理错误
  • 手动管理请求结果
  • 写法直观,适合普通请求

Suspense

jsx 复制代码
<Suspense fallback={<Loading />}>
  <Content />
</Suspense>

特点:

  • 由父组件统一提供 fallback
  • 多个子组件可以共享加载边界
  • 可以和 lazyuse、数据请求库结合
  • 更适合代码分割和支持 Suspense 的数据加载

十二、常见错误

1. 只使用 Suspense,不使用 lazy 或 Suspense 数据源

jsx 复制代码
<Suspense fallback={<Loading />}>
  <普通组件 />
</Suspense>

如果普通组件没有触发 Suspense,fallback 不会显示。

2. 在组件内部声明 lazy

错误:

jsx 复制代码
function App() {
  const Page = lazy(() => import('./Page.jsx'))

  return <Page />
}

正确:

jsx 复制代码
const Page = lazy(() => import('./Page.jsx'))

function App() {
  return <Page />
}

3. 把 Suspense 当成请求错误处理

jsx 复制代码
<Suspense fallback={<p>请求失败</p>}>
  <UserInfo />
</Suspense>

fallback 表示"还在加载",不是"请求失败"。

请求失败应该使用 Error Boundary 或普通错误状态处理。

4. Suspense 边界范围太大

jsx 复制代码
<Suspense fallback={<Loading />}>
  <整个页面 />
</Suspense>

这样只要其中一个区域加载,整个页面都可能显示 Loading。

应该根据页面结构划分边界:

jsx 复制代码
<Header />

<Suspense fallback={<UserSkeleton />}>
  <UserInfo />
</Suspense>

<Suspense fallback={<ListSkeleton />}>
  <ArticleList />
</Suspense>

十三、总结

Suspense 最常见的使用方式是:

jsx 复制代码
import { lazy, Suspense } from 'react'

const Page = lazy(() => import('./Page.jsx'))

function App() {
  return (
    <Suspense fallback={<p>加载中...</p>}>
      <Page />
    </Suspense>
  )
}

记住以下几点:

text 复制代码
Suspense 用于显示加载中的备用 UI
lazy 用于懒加载组件代码
useEffect 中的 fetch 不会自动触发 Suspense
use(Promise) 可以配合 Suspense 读取异步数据
ErrorBoundary 用于处理加载错误
startTransition 可以避免页面闪回 fallback

一句话记忆:

Suspense 就像一个加载边界:子组件还没准备好时显示 fallback,准备好后自动显示真实内容。

相关推荐
一心只读圣贤书1 小时前
AI 辅助前端空状态体验治理:从无数据页面到可行动引导
前端·人工智能
沐土Arvin1 小时前
音频h5录制开发
前端
PedroQue991 小时前
uni-app路由插件化:解锁高效开发新姿势
前端·uni-app
何时梦醒1 小时前
TypeScript 类型系统核心:type 与 interface 全方位深度对比(附实战案例)
前端·面试·typescript
渣波1 小时前
TS 必考题深度解析:type 与 interface 的终极对决
前端·javascript
书源1 小时前
AI 能写代码之后,前端工程师的价值在哪里?
前端·面试·程序员
恋猫de小郭1 小时前
Flutter iOS Deep Link 为什么会突然失效:一系列难以言喻的问题
android·前端·flutter
Full Stack Developme2 小时前
跨站脚本攻击 (XSS) 是什么 设计及工作原理
前端·xss
进击的蛋蛋2 小时前
JS对象拷贝
前端