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

同一函数或useEffect中设置两个state会引发组件无限重渲染吗?

问题原始代码
//ChatProvider.tsx
import { useRouter } from 'next/router'
import React, { createContext, ReactNode, useEffect, useState } from 'react'

export interface IUser {
  name: string
  email: string
  token: string
  pic: string
}

export interface userContextInterface {
  user: IUser | null
  isLoggedIn: boolean
  login: (_: IUser) => void
  logOut: () => void
}

const intialContext: userContextInterface = {
  user: null,
  isLoggedIn: false,
  login: (_: IUser) => null,
  logOut: () => null,
}

export const ChatContext = createContext<userContextInterface>(intialContext)

const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<IUser | null>(null)
  const [isLoggedIn, setIsLoggedIn] = useState(false)
  const router = useRouter()

  const loginUser = (userInfo: IUser) => {
    setUser(userInfo)
    setIsLoggedIn(true)
    localStorage.setItem('chatUserInfo', JSON.stringify(userInfo))
  }

  const logOut = () => {
    setIsLoggedIn(false)
    localStorage.removeItem('chatUserInfo')
    setUser(null)
  }
  //this is getting printed in a loop like 100 times
  console.log('hey context api is running')

  useEffect(() => {
    console.log('use effect is running')
    const storedUser = localStorage.getItem('chatUserInfo')
    console.log('is there user', storedUser)
    let userInfo: IUser | null = null
    if (storedUser) {
      userInfo = JSON.parse(localStorage.getItem('chatUserInfo') || '')
      setUser(userInfo)
    //commenting setIsLoggedIn below line fixes the issue
      setIsLoggedIn(true)
    } else {
      router.push('/')
    }
  }, [])

  return (
    <ChatContext.Provider
      value={{
        user,
        isLoggedIn,
        login: loginUser,
        logOut: logOut,
      }}
    >
      {children}
    </ChatContext.Provider>
  )
}

export default ChatProvider
问题原因说明
  • 同一同步逻辑(包括useEffect回调、普通事件回调)里连续调用多个setState完全是React允许的合法操作:React 18会自动把同一同步块里的多次setState合并成1次重渲染,React 17及更早版本最多触发2次重渲染,本身绝对不会导致无限循环。
  • 无限重渲染的本质是状态更新和路由副作用形成了死循环:
    1. 你把isLoggedIn做成了独立state,和user状态完全冗余,更新时需要两次setState,增加了重渲染次数。
    2. 每次组件渲染时,你传给ChatContext.Provider的value都是临时新建的对象,引用地址每次都变,会强制所有消费这个Context的组件重渲染——如果这些消费组件里包含路由守卫逻辑,就会触发路由校验。
    3. 路由校验过程中如果触发了根组件(也就是包裹ChatProvider的_app组件)重挂载,空依赖数组的useEffect会重新执行,再次读localStorage、更新状态、触发重渲染,循环就跑起来了。
    4. 注释掉setIsLoggedIn(true)循环就停,是因为少了这次状态更新后,isLoggedIn一直是初始值false,路由守卫不会触发后续的重挂载逻辑,循环链条直接断了。
  • 还有个隐藏问题:当localStorage没用户信息时,你直接调用router.push('/'),如果当前已经在首页路径,重复push会触发Next.js完整的路由重加载流程,也可能导致组件重挂载,放大循环问题。
修复方案(最佳实践)

核心思路是消除冗余状态、减少无意义重渲染、避免重复触发路由副作用,具体修改点如下:

  • 删掉冗余的isLoggedIn独立state:登录状态直接通过!!user派生就行,不用单独存,从根源上避免多次setState,也不会出现“user存在但isLoggedIn是false”这类状态不一致的bug。
  • 用useMemo缓存传给Context的value对象:只有当user等依赖项真的变了的时候才生成新的value,避免所有消费Context的组件跟着父组件无意义重渲染。
  • 路由跳转前加判断:只有当前不在目标路径的时候才调用router.push,避免重复跳转触发组件重挂载。
  • 加try/catch处理localStorage的JSON解析,防止本地存的损坏数据导致页面直接报错。

修复后的完整代码:

//ChatProvider.tsx
import { useRouter } from 'next/router'
import React, { createContext, ReactNode, useEffect, useState, useMemo } from 'react'

export interface IUser {
  name: string
  email: string
  token: string
  pic: string
}

export interface userContextInterface {
  user: IUser | null
  isLoggedIn: boolean
  login: (_: IUser) => void
  logOut: () => void
}

const intialContext: userContextInterface = {
  user: null,
  isLoggedIn: false,
  login: (_: IUser) => null,
  logOut: () => null,
}

export const ChatContext = createContext<userContextInterface>(intialContext)

const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<IUser | null>(null)
  const router = useRouter()
  // 直接从user派生登录状态,不需要单独维护
  const isLoggedIn = !!user

  const loginUser = (userInfo: IUser) => {
    setUser(userInfo)
    localStorage.setItem('chatUserInfo', JSON.stringify(userInfo))
  }

  const logOut = () => {
    localStorage.removeItem('chatUserInfo')
    setUser(null)
  }

  console.log('ChatProvider render')

  useEffect(() => {
    const storedUser = localStorage.getItem('chatUserInfo')
    if (storedUser) {
      try {
        const userInfo = JSON.parse(storedUser)
        setUser(userInfo)
      } catch (err) {
        // 处理本地数据损坏的异常场景
        localStorage.removeItem('chatUserInfo')
        if (router.pathname !== '/') {
          router.push('/')
        }
      }
    } else {
      // 避免重复跳转当前路由触发重挂载
      if (router.pathname !== '/') {
        router.push('/')
      }
    }
  }, [router])

  // 缓存context value,避免无意义重渲染
  const contextValue = useMemo(() => ({
    user,
    isLoggedIn,
    login: loginUser,
    logOut: logOut,
  }), [user, isLoggedIn])

  return (
    <ChatContext.Provider value={contextValue}>
      {children}
    </ChatContext.Provider>
  )
}

export default ChatProvider

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:00:57