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

Next.js客户端组件中tRPC useQuery加载状态的正确处理

解决Next.js客户端组件tRPC useQuery hydration失败问题

问题原因

Next.js服务端渲染(SSR)阶段,客户端组件中的useQuery不会发起请求,因此服务端渲染时isLoading始终为false。而客户端完成hydrate后,useQuery开始执行请求,isLoading变为true,此时客户端显示骨架屏,和服务端输出的UI(通常是null)不一致,触发hydration不匹配错误。

另外你的代码中,服务端无法访问localStorage,所以getDeviceId()初始返回空字符串,第一个useQuery的enabled为false,服务端渲染时直接走到return null分支;客户端hydrate后,localStorage有值时enabled变为true,isLoading变为true,显示骨架屏,这就导致前后UI不一致。

解决方案

方法1:使用useIsClient钩子判断客户端环境

创建一个钩子判断当前是否处于客户端,仅在客户端环境下才显示加载状态,确保服务端和客户端初始UI一致。

首先实现useIsClient钩子:

"use client";

import { useState, useEffect } from "react";

export function useIsClient() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return isClient;
}

修改你的组件:

"use client";

// 导入useIsClient
import { useIsClient } from "./useIsClient";

export function ActiveArtists() {
  const isClient = useIsClient();
  const { getValue: getDeviceId } = useLocalStorage({
    key: DEVICE_ID_LOCAL_STORAGE_KEY,
    defaultValue: "",
  });

  const { data: activeGrid, isLoading: isActiveGridLoading } =
    api.grids.getActiveGrid.useQuery(
      { device_id: getDeviceId() },
      { enabled: !!getDeviceId() },
    );

  const { data: artists, isLoading: isArtistsLoading } =
    api.grids.getArtists.useQuery(
      { grid_id: Number(activeGrid?.id) ?? 0 },
      { enabled: !!activeGrid?.id },
    );

  // 仅在客户端时才显示加载状态
  if (isClient && (isActiveGridLoading || isArtistsLoading)) {
    return (
      <div className="flex items-center space-x-4">
        <Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
        <Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
        <Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
      </div>
    );
  }

  if (!activeGrid?.id || !artists?.length) {
    return null;
  }

  const validArtists = artists.filter(
    (artist) => artist?.id && artist?.username,
  );

  return <ArtistList artists={validArtists} />;
}

方法2:用useEffect同步客户端加载状态

通过本地状态管理加载状态,利用useEffect仅在客户端执行的特性,同步tRPC的isLoading值,避免服务端和客户端初始UI差异。

修改组件:

"use client";

import { useState, useEffect } from "react";

export function ActiveArtists() {
  const [clientIsLoading, setClientIsLoading] = useState(false);
  const { getValue: getDeviceId } = useLocalStorage({
    key: DEVICE_ID_LOCAL_STORAGE_KEY,
    defaultValue: "",
  });

  const { data: activeGrid, isLoading: isActiveGridLoading } =
    api.grids.getActiveGrid.useQuery(
      { device_id: getDeviceId() },
      { enabled: !!getDeviceId() },
    );

  const { data: artists, isLoading: isArtistsLoading } =
    api.grids.getArtists.useQuery(
      { grid_id: Number(activeGrid?.id) ?? 0 },
      { enabled: !!activeGrid?.id },
    );

  // 仅在客户端同步加载状态
  useEffect(() => {
    setClientIsLoading(isActiveGridLoading || isArtistsLoading);
  }, [isActiveGridLoading, isArtistsLoading]);

  if (clientIsLoading) {
    return (
      <div className="flex items-center space-x-4">
        <Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
        <Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
        <Skeleton className={cn(avatarClasses.lg, "rounded-full")} />
      </div>
    );
  }

  if (!activeGrid?.id || !artists?.length) {
    return null;
  }

  const validArtists = artists.filter(
    (artist) => artist?.id && artist?.username,
  );

  return <ArtistList artists={validArtists} />;
}

原理说明

两种方法核心都是让服务端渲染时输出的UI和客户端初始hydrate时的UI保持一致:

  • 服务端渲染阶段,useEffect不会执行,本地状态保持初始值(false),所以服务端返回的是null;
  • 客户端hydrate完成后,useEffect执行,同步加载状态,此时才显示骨架屏,不会触发hydration不匹配错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 10:54:56