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

如何在NextJS中结合LocalStorage依赖正确使用Apollo Client的useQuery

问题分析与解决方案

核心问题

生产环境崩溃的根本原因是Next.js的服务器端渲染(SSR)特性:生产构建时组件会在服务器端执行一次,此时浏览器的localStorage不存在,导致user和services初始值为undefined,后续直接解构user.ciRif、访问services[0].id会抛出Cannot destructure property 'ciRif' of 'undefined'类错误。

开发环境正常是因为Next.js开发模式下默认采用客户端渲染(或热重载时localStorage已存在数据),不会触发服务器端的执行错误。

解决方案

1. 组件层面:延迟依赖LocalStorage的逻辑到客户端挂载后

用useState存储LocalStorage数据,通过useEffect在客户端挂载后读取,同时添加加载/空状态处理:

export default function UserDataUsage() {
  const [services, setServices] = useState(null);
  const [user, setUser] = useState(null);
  const [service, setService] = useState(null);
  const textColor = useColorModeValue("secondaryGray.900", "white");

  // 仅在客户端读取LocalStorage
  useEffect(() => {
    const storedServices = localStorage.getItem("services");
    const storedUser = localStorage.getItem("user");
    
    if (storedServices && storedUser) {
      const parsedServices = JSON.parse(storedServices);
      const parsedUser = JSON.parse(storedUser);
      
      setServices(parsedServices);
      setUser(parsedUser);
      setService(parsedServices[0]?.id); // 可选链避免数组为空报错
    }
  }, []);

  // 数据未就绪时显示加载状态
  if (!user || !services || !service) {
    return <Spinner />;
  }

  const { ciRif } = user;
  const { data: { contractDetails, dailyDataUsage, monthlyDataUsage, monthlyHistory, allServices }, error, loading } = useDataUsage(service, ciRif);

  if (loading) return <Spinner />;
  if (error) return <Text>加载失败,请重试</Text>;

  const { plan } = contractDetails;

  return (
    <Box pt={{ base: '130px', md: '80px', xl: '80px' }}>
      <>
        <SimpleGrid columns={1} gap='20px' mb='20px'>
          <Text color={textColor} fontSize='22px' fontWeight='700' lineHeight='100%'>
            Contratos
          </Text>
          <Select
            fontSize='sm'
            fontWeight='700'
            value={service}
            onChange={(e) => setService(e.target.value)}
          >
            {services.map((service) => (
              <option key={service.id} value={service.id}>Contrato #{service.id}</option>
            ))}
          </Select>
          <Text color={textColor} fontSize='22px' fontWeight='700' lineHeight='100%'>
            {plan}
          </Text>
        </SimpleGrid>

        <SimpleGrid columns={{ base: 1, md: 2, lg: 3, '2xl': 5 }} gap='20px' mb='20px'>
          <ContractDetails contractDetails={contractDetails} />
        </SimpleGrid>

        <SimpleGrid columns={{ base: 1, md: 2, xl: 2 }} gap='20px' mb='20px'>
          <TotalSpent monthlyHistory={monthlyHistory} />
          <DailyTraffic dailyDataUsage={dailyDataUsage} />
        </SimpleGrid>
        <SimpleGrid columns={{ base: 1, md: 1, xl: 2 }} gap='20px' mb='20px'>
          <DataUsage columnsData={columnsDataUsage} tableData={allServices} />
          <PieCard monthlyDataUsage={monthlyDataUsage} />
        </SimpleGrid>
      </>
    </Box>
  );
}

2. 自定义钩子层面:添加参数校验,避免无效查询

在useDataUsage中先校验参数是否有效,避免传入undefined导致GraphQL查询失败:

const useDataUsage = (service, ciRif) => {
  // 只有参数有效时才执行查询
  const { data: dataUsage, error: errorDataUsage, loading: loadingDataUsage } = useQuery(USER_DATA_USAGE, {
    variables: { serviceId: Number(service) },
    skip: !service // 参数为空时跳过查询
  });

  const { data: dataServices, error: errorServices, loading: loadingServices } = useQuery(SERVICES, {
    variables: { ciRif },
    skip: !ciRif // 参数为空时跳过查询
  });

  const date = new Date();
  let contractDetails, monthlyDataUsage, monthlyHistory, dailyDataUsage, allServices;

  if (!loadingDataUsage && !loadingServices && dataUsage && dataServices) {
    const { services: _services } = dataServices;
    const { services, dailyDataUsage: _dailyDataUsage, monthlyHistoryDataUsage } = dataUsage;
    
    // 可选链避免services为空报错
    const serviceData = services[0];
    if (!serviceData) return { data: {}, error: new Error("服务数据为空"), loading: false };

    const { idPlan: { download, upload, cap, plan }, dataUsage: { dataUsage: monthDataUsage } } = serviceData;

    dailyDataUsage = _dailyDataUsage.map((_day) => {
      const { day, dayname, dataUsage: dayDataUsage } = _day;
      return { day, dayname, dayDataUsage };
    });

    contractDetails = {
      download,
      upload,
      cap,
      plan,
      monthDataUsage,
      availableQuota: ((cap * 1000) - monthDataUsage)
    };

    monthlyDataUsage = {
      month: date.getMonth() + 1,
      monthDataUsage,
      cap: (cap * 1000)
    };

    monthlyHistory = monthlyHistoryDataUsage;

    allServices = _services.map((service) => {
      const { id, idPlan: { plan, cap }, dataUsage: { dataUsage } } = service;
      return {
        id,
        plan,
        quota: (cap / 1000000).toFixed(2),
        dataUsage: (dataUsage / 1000000000).toFixed(2),
        status: dataUsage >= (cap * 1000) ? "ACTIVO - LIMITADO" : "ACTIVO"
      };
    });
  }

  return {
    data: { contractDetails, dailyDataUsage, monthlyDataUsage, monthlyHistory, allServices },
    error: errorDataUsage || errorServices,
    loading: loadingDataUsage || loadingServices
  };
};

3. 额外优化:封装LocalStorage读取逻辑

可以封装一个自定义钩子统一处理LocalStorage读取,避免重复代码:

const useLocalStorage = (key, initialValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === "undefined") return initialValue;
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`读取LocalStorage key ${key}失败`, error);
      return initialValue;
    }
  });

  return storedValue;
};

// 在组件中使用:
const services = useLocalStorage("services", null);
const user = useLocalStorage("user", null);

关键注意点

  • Next.js中服务器端没有window对象,所有依赖浏览器API的逻辑必须放在useEffect或客户端专属代码块中
  • 永远要对从LocalStorage读取的数据做校验,避免JSON解析失败或数据为空的情况
  • 使用可选链操作符(?.)和空值合并运算符(??)可以有效避免空值访问报错

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 08:35:08