当你打开一个页面,看到骨架屏平滑过渡到真实内容,那背后很可能是 React.Suspense 和数据请求的合作。

🎯 Suspense 做什么?

  • 等待异步数据(组件、请求)准备好。
  • 数据还没回来时显示 fallback(骨架屏/加载动画)。
  • 数据准备好后自动替换成真实内容。

🧱 基础用法

<Suspense fallback={<p>加载中...</p>}>
  <UserProfile id={123} />
</Suspense>
  • UserProfile 内部如果“暂停”了(抛出一个 Promise),Suspense 会捕获并显示 fallback。

⚙️ 数据请求工具:自定义资源封装

function createResource(promise) {
  let status = 'pending';
  let result;
  const suspender = promise.then(
    (res) => {
      status = 'success';
      result = res;
    },
    (err) => {
      status = 'error';
      result = err;
    }
  );
  return {
    read() {
      if (status === 'pending') throw suspender;
      if (status === 'error') throw result;
      return result;
    },
  };
}
  • read() 会在数据没准备好时抛出 Promise,让 Suspense 接管。
  • 数据错误时抛出 Error,交给错误边界处理。

📦 创建用户数据资源

const userResource = createResource(
  fetch('/api/user/123').then((res) => res.json())
);

function UserProfile() {
  const user = userResource.read();
  return (
    <div>
      <h2>{user.name}</h2>
      <p>邮箱:{user.email}</p>
    </div>
  );
}

🧯 错误边界搭配

Suspense 只处理“等待”,错误要靠 Error Boundary。

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} onRetry={this.props.onRetry} />;
    }
    return this.props.children;
  }
}

🧩 完整示例:带重试按钮

function App() {
  const [resource, setResource] = useState(() => fetchUser());

  const retry = () => {
    setResource(fetchUser());
  };

  return (
    <ErrorBoundary onRetry={retry}>
      <Suspense fallback={<Skeleton />}> 
        <UserProfile resource={resource} />
      </Suspense>
    </ErrorBoundary>
  );
}

function UserProfile({ resource }) {
  const user = resource.read();
  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}

🎨 骨架屏写法

function Skeleton() {
  return (
    <div className="skeleton">
      <div className="avatar" />
      <div className="line" />
      <div className="line short" />
    </div>
  );
}
  • 通过 CSS 动画模拟 “骨架闪烁” 效果。
  • fallback 可以是骨架屏、loading、占位图,灵活选择。

⚙️ 和 React Query/Relay 等库搭配

  • React Query:<Suspense> 处理 loading,useQuery 里开启 suspense: true
  • Relay:天然支持 Suspense,只需用 <Suspense> 包裹数据组件。
  • SWR:suspense: true + <Suspense> 即可。
function UserProfile({ id }) {
  const { data } = useQuery(['user', id], fetchUser, { suspense: true });
  return <div>{data.name}</div>;
}

🧯 常见坑

现象 解决
Suspense 没生效 组件没抛 Promise 确保数据加载时 throw Promise(库需开启 suspense)
错误没显示 没有 ErrorBoundary Suspense 只处理 pending 状态
fallback 闪烁 请求太快 加延迟:setTimeoutstartTransition
多层 Suspense 嵌套混乱 fallback 嵌套难维护 使用多个 Suspense 包裹不同区域

🏁 小练习

  1. Suspense 包裹多个组件:用户信息 + 动态列表,分别加载。
  2. 写一个 useResource Hook 封装 createResource,复用在项目中。
  3. 研究 Next.js 13 的 app 目录,看看它如何内置 Suspense 数据加载。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐