Preact/Fresh使用Context报错:Converting circular structure to JSON如何解决?
在Fresh/Preact中使用Context解决"Converting circular structure to JSON"错误
问题背景
我在Fresh/Preact应用里创建了一个全局状态Context,但渲染时抛出Converting circular structure to JSON错误。我知道这是Fresh的服务端渲染(SSR)导致的——AppContext.Provider的value里包含函数,序列化时会因为函数闭包的自引用触发循环结构错误。我试过用Preact signals,但遇到了其他问题,希望保留Context方案(熟悉Next.js的Context用法)。
我的AppContext代码如下:
import { createContext } from "preact" import { useContext, useState } from "preact/hooks" export type Theme = "light" | "dark" export type Language = "english" | "norsk" interface IAppContext { theme: Theme, language: Language, toggleTheme: () => void, toggleLanguage: () => void } const AppContext = createContext<IAppContext>({ theme: "light", language: "english", toggleLanguage: () => {}, toggleTheme: () => {} }) export const useAppContext = () => useContext(AppContext) export default function AppContextProvider({ children }: any) { const [theme, setTheme] = useState<Theme>("light") const [language, setLanguage] = useState<Language>("english") const toggleTheme = () => setTheme(_theme => _theme === "light" ? "dark" : "light") const toggleLanguage = () => setLanguage(_language => _language === "english" ? "norsk" : "english") const context: IAppContext = { theme, language, toggleTheme, toggleLanguage } return <AppContext.Provider value={context}>{children}</AppContext.Provider> }
解决思路
Fresh的SSR会把组件树序列化用于客户端hydration,而Context的value里包含的函数(比如toggleTheme)会携带闭包引用(比如引用组件内部的setTheme),这些引用会形成循环结构,导致JSON序列化失败。
要保留Context方案,核心是让包含函数的Context Provider只在客户端渲染,避免服务端尝试序列化它。
具体实现
步骤1:将Context Provider改为客户端组件
创建一个客户端专属的Provider组件,确保它只在浏览器环境中运行:
// components/ClientAppContextProvider.tsx /** @jsx h */ import { createContext } from "preact" import { useContext, useState } from "preact/hooks" export type Theme = "light" | "dark" export type Language = "english" | "norsk" interface IAppContext { theme: Theme, language: Language, toggleTheme: () => void, toggleLanguage: () => void } const AppContext = createContext<IAppContext>({ theme: "light", language: "english", toggleLanguage: () => {}, toggleTheme: () => {} }) export const useAppContext = () => useContext(AppContext) export default function ClientAppContextProvider({ children }: { children: preact.ComponentChildren }) { // 客户端初始化时读取本地存储,保持状态持久化(可选) const [theme, setTheme] = useState<Theme>(() => { if (typeof window !== "undefined") { return localStorage.getItem("theme") as Theme || "light" } return "light" }) const [language, setLanguage] = useState<Language>(() => { if (typeof window !== "undefined") { return localStorage.getItem("language") as Language || "english" } return "english" }) const toggleTheme = () => { setTheme(prev => { const newTheme = prev === "light" ? "dark" : "light" localStorage.setItem("theme", newTheme) return newTheme }) } const toggleLanguage = () => { setLanguage(prev => { const newLang = prev === "english" ? "norsk" : "english" localStorage.setItem("language", newLang) return newLang }) } const context: IAppContext = { theme, language, toggleTheme, toggleLanguage } return <AppContext.Provider value={context}>{children}</AppContext.Provider> }
步骤2:在_app.tsx中用ClientOnly包裹Provider
Fresh的ClientOnly组件会在服务端只渲染fallback内容,客户端加载完成后再渲染内部的客户端组件,这样服务端不会序列化Provider里的函数:
// routes/_app.tsx import { AppProps } from "$fresh/server.ts" import { ClientOnly } from "$fresh/runtime.ts" import ClientAppContextProvider from "../components/ClientAppContextProvider.tsx" export default function App({ Component, pageProps }: AppProps) { return ( <ClientOnly fallback={<div>Loading...</div>}> <ClientAppContextProvider> <Component {...pageProps} /> </ClientAppContextProvider> </ClientOnly> ) }
步骤3:在使用Context的组件中兼容服务端渲染
如果使用useAppContext的组件是服务端渲染的,需要判断环境,避免在服务端调用客户端才有的Context方法:
// components/ThemeToggle.tsx /** @jsx h */ import { useAppContext } from "../components/ClientAppContextProvider.tsx" import { useIsClient } from "$fresh/runtime.ts" export default function ThemeToggle() { const isClient = useIsClient() const { theme, toggleTheme } = useAppContext() if (!isClient) { // 服务端渲染静态占位按钮 return <button>切换主题</button> } return ( <button onClick={toggleTheme}> 切换到{theme === "light" ? "深色" : "浅色"}模式 </button> ) }
为什么这样有效
ClientOnly组件确保服务端不会渲染包含函数的Context Provider,避免了序列化循环结构的问题。- 客户端组件中的状态和函数只会在浏览器环境中初始化和运行,符合Preact Context的使用逻辑。
- 可选的本地存储读取让状态在页面刷新后得以保留,提升用户体验。
内容的提问来源于stack exchange,提问作者Noobster
相关产品推荐
相关产品推荐

