如何在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
相关产品推荐
相关产品推荐

