Next.js项目useEffect未按预期触发导致收件箱页面空白
问题描述
我开发了一个Inbox(收件箱)页面,会根据当前屏幕尺寸,分别渲染适配移动端或桌面端的收件箱界面。
目前页面存在偶发无法正常渲染消息的问题,经排查问题根源出在useEffect的执行逻辑上。
以下是Inbox组件的完整代码:
import { useEffect } from "react"; import { getUserConversations } from "@services/firebase/client/chat-client-services"; import userDataStore from "@stores/UserState/UserStore"; import InboxMessagePreview from "@components/Inbox/InboxMessagePreview/InboxMessagePreview"; import authStore from "@stores/AuthState/AuthStore"; import useSWR from "swr"; import Chat from "@components/Chat/Chat"; import Router, { useRouter } from "next/router"; import useWindowSize from "hooks/useWindowSize"; import { classNames } from "helpers/helpers"; import Skeleton from "@components/UI/Loading/Skeleton/Skeleton"; export default function Inbox() { const size = useWindowSize(); const currentUser = authStore((state) => state.currentUser); const userData = userDataStore((state) => state.userData); const { data, error } = useSWR(currentUser?.uid, getUserConversations); const router = useRouter(); console.log({ data }); console.log({ size }); useEffect(() => { if ( !Router?.query?.chat && data?.conversations?.length > 0 && size.width >= 768 ) { console.log("IN USE EFFECT IF"); Router.push( { pathname: `/inbox`, query: { chat: data.conversations[0].docID }, }, undefined, { shallow: true } ); } }, [data, size]); if (error) { <p>Error</p>; } if (!data) { return <InboxSkeleton />; } if (size?.width < 768 && data) { return ( <MobileChat currentUser={currentUser} data={data} query={router?.query} userData={userData} /> ); } if (size?.width >= 768 && data) { return ( <DesktopChat currentUser={currentUser} data={data} query={router?.query} userData={userData} /> ); } return null; }
页面加载时,useEffect内部的console.log有时不会触发,导致消息无法加载,页面显示空白。
我初步猜测问题要么出在useWindowSize()自定义Hook上,要么和Next.js的Router使用方式有关。
我同时引入useRouter和Router的原因是:需要在useEffect内执行路由跳转操作,如果在useEffect内调用router.push(),React会发出警告,要求将router加入useEffect的依赖数组;但如果将router加入依赖数组,useEffect内执行路由跳转就会更新router对象,进而触发useEffect重复执行,造成无限循环问题。
项目中用到的useWindowSize自定义Hook代码如下:
import { useState, useEffect } from "react"; // Define general type for useWindowSize hook, which includes width and height interface Size { width: number | undefined; height: number | undefined; } function useWindowSize(): Size { const [windowSize, setWindowSize] = useState<Size>({ width: undefined, height: undefined, }); useEffect(() => { // Handler to call on window resize function handleResize() { // Set window width/height to state setWindowSize({ width: window.innerWidth, height: window.innerHeight, }); } // Add event listener window.addEventListener("resize", handleResize); // Call handler right away so state gets updated with initial window size handleResize(); // Remove event listener on cleanup return () => window.removeEventListener("resize", handleResize); }, []); // Empty array ensures that effect is only run on mount return windowSize; } export default useWindowSize;
根因分析
偶发白屏、跳转逻辑不触发是4个代码问题共同导致的:
- 错误分支漏写返回:
if (error) { <p>Error</p>; }没有return关键字,接口报错时不会渲染错误提示,会直接走到最后返回null白屏。 - 尺寸初始值空窗:
useWindowSize首次返回的width是undefined,既不满足<768的移动端判断,也不满足>=768的桌面端判断,直接返回null。如果后续跳转逻辑没触发,就会一直停留在白屏状态。 - 全局Router使用错误:effect里调用的全局导入
Router和组件渲染周期不同步,Next.js hydration阶段它的query字段经常是未初始化状态,导致!Router.query.chat的判断结果不准。另外「把useRouter返回的router加入依赖会无限循环」的认知是错误的:Next.js已经保证useRouter返回的router实例引用稳定,不会因为路由跳转改变引用,加入依赖数组不会触发循环执行。 - 缺少路由就绪判断:Next.js首次加载时,hydration完成前路由参数是未解析完成的,这时候读取query会拿到空值。等参数解析完成后,effect依赖里没有router相关状态,不会重新执行判断,自动选中第一个会话的逻辑就直接错过了执行时机。
修复方案
1. 修正Inbox组件逻辑
- 移除全局
Router导入,统一使用useRouter返回的实例 - 加入
router.isReady判断,等路由解析完成后再执行自动跳转 - 补全错误分支的return
- 给尺寸判断加兜底,避免width为undefined时返回null
- 补全useEffect全量依赖,不需要担心无限循环问题
修正后的核心代码:
export default function Inbox() { const size = useWindowSize(); const currentUser = authStore((state) => state.currentUser); const userData = userDataStore((state) => state.userData); const { data, error } = useSWR(currentUser?.uid, getUserConversations); const router = useRouter(); useEffect(() => { // 路由未就绪直接跳过 if (!router.isReady) return; // 已选中聊天、无会话列表、非桌面端 不需要跳转 if ( router.query.chat || !data?.conversations?.length || !size.width || size.width < 768 ) return; router.push( { pathname: `/inbox`, query: { chat: data.conversations[0].docID }, }, undefined, { shallow: true } ); // router引用稳定,加入依赖不会触发循环 }, [data, size, router]); if (error) { return <p>加载失败,请刷新重试</p>; } // 尺寸未初始化时和加载态共用骨架屏 if (!data || !size.width) { return <InboxSkeleton />; } const isMobile = size.width < 768; const ChatComponent = isMobile ? MobileChat : DesktopChat; return ( <ChatComponent currentUser={currentUser} data={data} query={router.query} userData={userData} /> ); }
2. 可选优化useWindowSize
给初始值加客户端默认判断,减少首次渲染的空窗期,非必须,上面的组件逻辑已经兼容width为undefined的场景:
const [windowSize, setWindowSize] = useState<Size>(() => ({ width: typeof window !== 'undefined' ? window.innerWidth : undefined, height: typeof window !== 'undefined' ? window.innerHeight : undefined, }));
内容的提问来源于stack exchange,提问作者squish
相关产品推荐
相关产品推荐

