React的 Suspense

React的内置组件 中文意为 悬念 ,所以表现的是一种拿不准的状态,该组件的作用是,作为一个包裹组件,包裹我们需要加载的子级组件,在子级组件未加载完毕的时候展示替代组件(比如loading组件)

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>

本文直接对目前Suspense的使用方法做一个直白讲解,Suspense的演变过程以及理念可以看文末的参考链接详细了解。

常用用法

Suspense 结合Data Fetch

在我刚开始学习React的时候,Suspense还只能React.lazy结合使用用于加载异步组件。而现在Suspense已经可以对任何异步状态生效了,当然这需要遵守一定的规则。

Suspense结合 Data Fetch的代码如下:

import { fetchData } from './data';

export default function Albums() {
  const albums = use(fetchData(`/the-beatles/album`));
  console.log('albums', albums);
  return (
    <ul>
      {albums.map((album) => (
        <li key={album.id}>
          {album.title} ({album.year})
        </li>
      ))}
    </ul>
  );
}
// This is a workaround for a bug to get the demo running.
// TODO: replace with real implementation when the bug is fixed.
function use(promise) {
  console.log('promise', promise);
  if (promise.status === 'fulfilled') {
    return promise.value;
  } else if (promise.status === 'rejected') {
    throw promise.reason;
  } else if (promise.status === 'pending') {
    throw promise;
  } else {
    promise.status = 'pending';
    promise.then(
      (result) => {
        promise.status = 'fulfilled';
        promise.value = result;
      },
      (reason) => {
        promise.status = 'rejected';
        promise.reason = reason;
      },
    );
    console.log('throw', promise);
    throw promise;
  }
}

注意上述代码中的use方法就是将我们的异步请求方法转换为能够符合suspense用法的函数,即要求被Suspense包裹的组件是能够抛出一个Promise 异常。在这个 Promise 结束后再渲染组件,因此use函数需要在 Pending 状态时抛出一个 Promise,使其可以被 Suspense 捕获到。

Suspense和lazy结合使用

这个就是最开始大家都了解的使用方法了

import React, { Suspense, lazy } from 'react';

const AsynComponent = lazy(
  async () => await lazyLoad(import('./lazyLoad')),
);
export default function Index() {
  return (
    <Suspense fallback={<div>loading....</div>}>
      <AsynComponent />
    </Suspense>
  );
}

// 模拟加载组件的延迟
async function lazyLoad(promise) {
  await new Promise((resolve) => {
    setTimeout(resolve, 2000);
  });
  return promise;
}

动态加载外部组件

最近工作中有做到在pc端动态加载H5组件资产的需求,用于展示发布的组件。这里我利用suspense和错误边界来进行一个封装,让其成为一个通用的组件。

代码示例:

错误边界组件

这里按照需求自定义即可

// ErrorBoundary.tsx
import React, { ErrorInfo, ReactNode } from 'react';

interface Props {
  fallback: ReactNode;
  children: ReactNode;
}

interface State {
  hasError: boolean;
}

export default class ErrorBoundary extends React.Component<Props, State> {
  state: State = { hasError: false };

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

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

动态加载组件

import React, { Suspense, useRef } from 'react';
import ReactDOM from 'react-dom';
import ErrorBoundary from './ErrorBoundary';

// 手动定义全局变量 保证umd方式导入的组件加载正常
window.React = React;
window.ReactDOM = ReactDOM;

interface Props {
  src: string; // UMD 脚本地址
  globalName: string; // UMD 暴露的全局变量名
  fallback?: React.ReactNode; // 加载态
  errorFallback?: React.ReactNode; // 错误态
  componentProps?: Record<string, unknown>; // 组件的props
}

interface Thenable<T> extends PromiseLike<T> {
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?:
      | ((value: T) => TResult1 | PromiseLike<TResult1>)
      | undefined
      | null,
    onrejected?:
      | ((reason: any) => TResult2 | PromiseLike<TResult2>)
      | undefined
      | null,
  ): Thenable<TResult1 | TResult2>;
  reason?: string;
  status?: 'rejected' | 'fulfilled' | 'pending';
  value?: unknown;
}

const cache = new Map();
export function loadComponent(
  src: string,
  getData: (src: string) => Promise<unknown>,
) {
  if (!cache.has(src)) cache.set(src, getData(src));
  return cache.get(src);
}

function use<T>(promise: Thenable<T>) {
  // console.log('promise', promise);
  if (promise.status === 'fulfilled') {
    return promise.value;
  } else if (promise.status === 'rejected') {
    throw promise.reason;
  } else if (promise.status === 'pending') {
    throw promise;
  } else {
    promise.status = 'pending';
    promise.then(
      (result) => {
        promise.status = 'fulfilled';
        promise.value = result;
      },
      (reason) => {
        promise.status = 'rejected';
        promise.reason = reason;
      },
    );
    // console.log('throw', promise);
    throw promise;
  }
}

export const UmdComponent: React.FC<Props> = ({
  src,
  globalName,
  componentProps,
}) => {
  const abortControllerRef = useRef<AbortController | null>(null);
  const scriptRef = useRef<HTMLScriptElement | null>(null);

  function loadScript(src: string): Promise<React.ElementType> {
    abortControllerRef.current?.abort();
    abortControllerRef.current = new AbortController();
    const { signal } = abortControllerRef.current;
    return new Promise((resolve, reject) => {
      // 1. 检查是否已加载
      if (scriptRef.current) {
        // 清除脚本
        document.head.removeChild(scriptRef.current);
        scriptRef.current = null;
      }
      // 2. 动态插入 <script>
      const script = document.createElement('script');
      script.id = 'globalName';
      script.src = src;
      script.async = true;
      
      // 3. 加载回调
      const onLoad = () => {
        if (signal.aborted) return;
        const ctor = (window as any)[globalName];
        if (!ctor) {
          reject(new Error(`UMD global "${globalName}" not found.`));
          return;
        }
        resolve(ctor);
      };

      // 4. 错误回调
      const onError = (event: Event | Error) => {
        console.log(event);
        reject(new Error('Script load failed'));
      };

      script.addEventListener('load', onLoad);
      script.addEventListener('error', onError);

      document.head.appendChild(script);
    });
  }

  const Component = use(loadComponent(src, loadScript)) as React.ElementType;

  return <Component {...componentProps} />;
};

// 包装器:Suspense + ErrorBoundary
export const UmdComponentLoader: React.FC<Props> = (props) => (
  <ErrorBoundary fallback={props.errorFallback}>
    <Suspense fallback={props.fallback}>
      <UmdComponent {...props} />
    </Suspense>
  </ErrorBoundary>
);

组件使用

import React from 'react';
import { UmdComponentLoader } from './umd';
// 链接测试时请自行替换为真实umd方式的cdn
export default function Index() {
  return (
    <div>
      <UmdComponentLoader
        src="https://xxx/umd-named/index.umd.min.js"
        globalName="xxx"
        componentProps={{
        }}
        fallback={<>加载中。。。。</>}
        errorFallback={<>加载失败。。。。</>}
      />
    </div>
  );
}

存在的问题

  1. fetch库支持情况:目前Suspense 的Data Fetch还是处于比较新的一个阶段,可以看到 一些 支持Suspense的框架都暂时不推荐在生产环境使用其Suspense模式,比如 swr和react-query,不过我们只要了解这个规则使用,自己简单的使用,比如 自定义类似与use(React19会提供这个方法)的方法 那也是没有问题的。当然还是要关注最新的变动。 swr React-query
  2. 一闪而过的fallback:Suspense目前也存在问题就是 加载闪烁的问题,这个问题在以前结合lazy异步加载组件的时候经常遇到,如果第一次进入页面,组件加载的很快,那我们就会看到一闪而过的loading,这对用户来讲是十分糟糕的体验。我们比如可以考虑为Suspense设置一些新的属性,用于控制fallback 的最小出现时刻,或者控制fallback至少显示多长时间。目前Suspense还未支持这一特性,当然期待未来会支持这个特效。github上也有这个issue

当然我们可以对fallback组件进行改造来支持这一特性

<Suspense fallback={MyFallback} />;
const MyFallback = () => {
  // 计时器,200 ms 以内 return null,200 ms 后 return <Spin />(也可以结合设置最短展示Spin多少ms)
}

附录代码

// fetchData --data
// Note: the way you would do data fetching depends on
// the framework that you use together with Suspense.
// Normally, the caching logic would be inside a framework.

const cache = new Map();

// 注意这个缓存函数,如果不设置缓存的,use函数每次都会生成一个新的promise,导致永远无法更新promise状态,进而导致无限渲染
export function fetchData(url) {
  if (!cache.has(url)) {
    cache.set(url, getData(url));
  }
  return cache.get(url);
}

async function getData(url) {
  if (url === '/the-beatles/albums') {
    return await getAlbums();
  } else {
    throw Error('Not implemented');
  }
}

async function getAlbums() {
  // Add a fake delay to make waiting noticeable.
  await new Promise((resolve) => {
    setTimeout(resolve, 2000);
  });

  return [
    {
      id: 13,
      title: 'Let It Be',
      year: 1970,
    },
    {
      id: 12,
      title: 'Abbey Road',
      year: 1969,
    },
    {
      id: 11,
      title: 'Yellow Submarine',
      year: 1969,
    },
    {
      id: 10,
      title: 'The Beatles',
      year: 1968,
    },
    {
      id: 9,
      title: 'Magical Mystery Tour',
      year: 1967,
    },
    {
      id: 8,
      title: "Sgt. Pepper's Lonely Hearts Club Band",
      year: 1967,
    },
    {
      id: 7,
      title: 'Revolver',
      year: 1966,
    },
    {
      id: 6,
      title: 'Rubber Soul',
      year: 1965,
    },
    {
      id: 5,
      title: 'Help!',
      year: 1965,
    },
    {
      id: 4,
      title: 'Beatles For Sale',
      year: 1964,
    },
    {
      id: 3,
      title: "A Hard Day's Night",
      year: 1964,
    },
    {
      id: 2,
      title: 'With The Beatles',
      year: 1963,
    },
    {
      id: 1,
      title: 'Please Please Me',
      year: 1963,
    },
  ];
}

参考文章

  1. https://react.dev/reference/react/Suspense
  2. https://juejin.cn/post/6844904093463363591#heading-0
  3. https://indepth.dev/posts/1044/why-react-suspense-will-be-a-game-changer
Last modification:July 5, 2025
如果觉得我的文章对你有用,请随意赞赏