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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:51:17