fix: Resolve infinite render loop by memoizing context functions and values

This commit is contained in:
Yunxiao Xu
2026-02-17 02:43:19 -08:00
parent 23471350df
commit 1b15a4e18c
2 changed files with 40 additions and 26 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, createContext, useContext } from "react" import { useState, useEffect, createContext, useContext, useMemo, useCallback } from "react"
import { Routes, Route } from "react-router-dom" import { Routes, Route } from "react-router-dom"
import { MainLayout } from "./components/layout/MainLayout" import { MainLayout } from "./components/layout/MainLayout"
import { LoginForm } from "./components/auth/LoginForm" import { LoginForm } from "./components/auth/LoginForm"
@@ -25,8 +25,15 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false) const [isAuthenticated, setIsAuthenticated] = useState(false)
const [user, setUser] = useState<UserResponse | null>(null) const [user, setUser] = useState<UserResponse | null>(null)
const value = useMemo(() => ({
isAuthenticated,
setIsAuthenticated,
user,
setUser
}), [isAuthenticated, user])
return ( return (
<AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated, user, setUser }}> <AuthContext.Provider value={value}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
) )
@@ -81,16 +88,16 @@ function AppContent() {
initAuth() initAuth()
}, [setIsAuthenticated, setThemeLocal, setUser]) }, [setIsAuthenticated, setThemeLocal, setUser])
const loadHistory = async () => { const loadHistory = useCallback(async () => {
try { try {
const history = await ChatService.listConversations() const history = await ChatService.listConversations()
setConversations(history) setConversations(history)
} catch (err) { } catch (err) {
console.error("Failed to load conversation history:", err) console.error("Failed to load conversation history:", err)
} }
} }, [])
const handleAuthSuccess = async () => { const handleAuthSuccess = useCallback(async () => {
try { try {
const userData = await AuthService.getMe() const userData = await AuthService.getMe()
setUser(userData) setUser(userData)
@@ -102,9 +109,9 @@ function AppContent() {
} catch (err: unknown) { } catch (err: unknown) {
console.error("Failed to fetch user profile after login:", err) console.error("Failed to fetch user profile after login:", err)
} }
} }, [setUser, setIsAuthenticated, setThemeLocal, loadHistory])
const handleLogout = async () => { const handleLogout = useCallback(async () => {
try { try {
await AuthService.logout() await AuthService.logout()
} catch (err: unknown) { } catch (err: unknown) {
@@ -117,9 +124,9 @@ function AppContent() {
setThreadMessages({}) setThreadMessages({})
setThemeLocal("light") setThemeLocal("light")
} }
} }, [setIsAuthenticated, setUser, setThemeLocal])
const handleSelectConversation = async (id: string) => { const handleSelectConversation = useCallback(async (id: string) => {
setSelectedThreadId(id) setSelectedThreadId(id)
try { try {
const msgs = await ChatService.getMessages(id) const msgs = await ChatService.getMessages(id)
@@ -127,9 +134,9 @@ function AppContent() {
} catch (err) { } catch (err) {
console.error("Failed to fetch messages:", err) console.error("Failed to fetch messages:", err)
} }
} }, [])
const handleCreateConversation = async () => { const handleCreateConversation = useCallback(async () => {
try { try {
const newConv = await ChatService.createConversation() const newConv = await ChatService.createConversation()
setConversations(prev => [newConv, ...prev]) setConversations(prev => [newConv, ...prev])
@@ -138,18 +145,18 @@ function AppContent() {
} catch (err) { } catch (err) {
console.error("Failed to create conversation:", err) console.error("Failed to create conversation:", err)
} }
} }, [])
const handleRenameConversation = async (id: string, name: string) => { const handleRenameConversation = useCallback(async (id: string, name: string) => {
try { try {
const updated = await ChatService.renameConversation(id, name) const updated = await ChatService.renameConversation(id, name)
setConversations(prev => prev.map(c => c.id === id ? updated : c)) setConversations(prev => prev.map(c => c.id === id ? updated : c))
} catch (err) { } catch (err) {
console.error("Failed to rename conversation:", err) console.error("Failed to rename conversation:", err)
} }
} }, [])
const handleDeleteConversation = async (id: string) => { const handleDeleteConversation = useCallback(async (id: string) => {
try { try {
await ChatService.deleteConversation(id) await ChatService.deleteConversation(id)
setConversations(prev => prev.filter(c => c.id !== id)) setConversations(prev => prev.filter(c => c.id !== id))
@@ -164,11 +171,11 @@ function AppContent() {
} catch (err) { } catch (err) {
console.error("Failed to delete conversation:", err) console.error("Failed to delete conversation:", err)
} }
} }, [selectedThreadId])
const handleMessagesFinal = (id: string, messages: MessageResponse[]) => { const handleMessagesFinal = useCallback((id: string, messages: MessageResponse[]) => {
setThreadMessages(prev => ({ ...prev, [id]: messages })) setThreadMessages(prev => ({ ...prev, [id]: messages }))
} }, [])
const queryParams = new URLSearchParams(window.location.search) const queryParams = new URLSearchParams(window.location.search)
const externalError = queryParams.get("error") const externalError = queryParams.get("error")

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useState } from "react" import { createContext, useContext, useEffect, useState, useCallback, useMemo } from "react"
import { AuthService } from "@/services/auth" import { AuthService } from "@/services/auth"
export type Theme = "light" | "dark" export type Theme = "light" | "dark"
@@ -29,7 +29,7 @@ export function ThemeProvider({
root.classList.add(theme) root.classList.add(theme)
}, [theme]) }, [theme])
const setTheme = async (newTheme: Theme) => { const setTheme = useCallback(async (newTheme: Theme) => {
setThemeState(newTheme) setThemeState(newTheme)
if (isAuthenticated) { if (isAuthenticated) {
try { try {
@@ -38,18 +38,25 @@ export function ThemeProvider({
console.error("Failed to sync theme to backend:", error) console.error("Failed to sync theme to backend:", error)
} }
} }
} }, [isAuthenticated])
const setThemeLocal = (newTheme: Theme) => { const setThemeLocal = useCallback((newTheme: Theme) => {
setThemeState(newTheme) setThemeState(newTheme)
} }, [])
const toggleTheme = () => { const toggleTheme = useCallback(() => {
setTheme(theme === "light" ? "dark" : "light") setTheme(theme === "light" ? "dark" : "light")
} }, [theme, setTheme])
const value = useMemo(() => ({
theme,
setTheme,
setThemeLocal,
toggleTheme
}), [theme, setTheme, setThemeLocal, toggleTheme])
return ( return (
<ThemeContext.Provider value={{ theme, setTheme, setThemeLocal, toggleTheme }}> <ThemeContext.Provider value={value}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
) )