NextJS 13 App目录项目用Chakra UI出现FOUC及控制台错误
问题描述
基于NextJS 13 App目录搭建的项目,使用Chakra UI框架,未登录时middleware会将请求重定向到登录页,但存在以下问题:
- 登录页出现未样式化内容闪烁(FOUC)
- 控制台报错
- 登录页目录中的加载组件从未显示
相关代码
package.json
{ "dependencies": { "@chakra-ui/pro-theme": "^0.0.57", "@chakra-ui/react": "^2.4.3", "@emotion/cache": "^11.10.5", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@types/node": "18.11.10", "@types/react": "18.0.25", "@types/react-dom": "18.0.9", "cookies-next": "^2.1.1", "eslint": "8.28.0", "eslint-config-next": "13.0.5", "framer-motion": "^6.5.1", "next": "13.0.5", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.7.1", "typescript": "4.9.3" } }
middleware.ts
import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import hasSession from "./helpers/gate/hasSession"; export const config = { matcher: [ "/((?!login|favicon.ico).*)", ], }; export function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith("/_next/")) { return NextResponse.next(); } if (hasSession(request)) { return NextResponse.next(); } const loginUrl = new URL("/login", request.url); return NextResponse.redirect(loginUrl); }
app/layout.tsx
import Chakra from "../lib/ChakraProvider"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <head /> <body> <Chakra>{children}</Chakra> </body> </html> ); }
lib/ChakraProvider.tsx
"use client"; import { ChakraProvider, ColorModeScript } from "@chakra-ui/react"; import { theme as proTheme } from "@chakra-ui/pro-theme"; export default function Chakra({ children }: { children: React.ReactNode }) { return ( <ChakraProvider theme={proTheme}> <ColorModeScript initialColorMode={"dark"} /> {children} </ChakraProvider> ); }
问题根源
- FOUC与控制台报错:ChakraProvider是客户端组件,根布局为服务端组件,导致Chakra样式在客户端hydration前未加载;ColorModeScript放在ChakraProvider内部,执行时机过晚,加剧样式闪烁。
- 加载组件不显示:middleware是服务端层面的重定向,登录页的
loading.tsx仅在客户端导航或组件异步加载时触发,服务端跳转不会激活它。
解决方案
1. 修复FOUC与控制台报错
调整ChakraProvider结构与样式加载
- 将ColorModeScript移至根布局的
<head>中,确保页面渲染前执行 - 创建服务端兼容的Emotion缓存,避免客户端与服务端样式不匹配
修改lib/ChakraProvider.tsx:
"use client"; import { ChakraProvider } from "@chakra-ui/react"; import { theme as proTheme } from "@chakra-ui/pro-theme"; import { CacheProvider } from "@emotion/react"; import createCache from "@emotion/cache"; // 创建服务端兼容的Emotion缓存 const clientEmotionCache = createCache({ key: "chakra-ui-emotion-cache", }); export default function Chakra({ children }: { children: React.ReactNode }) { return ( <CacheProvider value={clientEmotionCache}> <ChakraProvider theme={proTheme}>{children}</ChakraProvider> </CacheProvider> ); }
修改app/layout.tsx:
import Chakra from "../lib/ChakraProvider"; import { ColorModeScript } from "@chakra-ui/react"; import { theme as proTheme } from "@chakra-ui/pro-theme"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <head> {/* 提前注入ColorModeScript,避免主题样式闪烁 */} <ColorModeScript initialColorMode={proTheme.config.initialColorMode} /> </head> <body> <Chakra>{children}</Chakra> </body> </html> ); }
2. 让登录页显示加载状态
由于服务端重定向不会触发loading.tsx,可在登录页组件中添加客户端加载逻辑:
修改app/login/page.tsx(示例):
"use client"; import { useState, useEffect } from "react"; import { Box, Spinner, Heading, Button, Input } from "@chakra-ui/react"; export default function LoginPage() { const [isLoading, setIsLoading] = useState(true); useEffect(() => { // 模拟初始化逻辑(如检查缓存、加载配置) setTimeout(() => { setIsLoading(false); }, 400); }, []); if (isLoading) { return ( <Box display="flex" justifyContent="center" alignItems="center" minH="100vh"> <Spinner size="xl" color="blue.500" /> </Box> ); } return ( <Box display="flex" flexDir="column" gap={4} justifyContent="center" alignItems="center" minH="100vh" p={4} > <Heading size="lg">登录</Heading> <Input placeholder="用户名" w="sm" /> <Input placeholder="密码" type="password" w="sm" /> <Button colorScheme="blue">登录</Button> </Box> ); }
3. 优化Middleware规则
简化静态资源拦截逻辑,通过matcher直接排除无需校验的路径:
修改middleware.ts:
import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import hasSession from "./helpers/gate/hasSession"; export const config = { matcher: [ "/((?!login|favicon.ico|_next/static|_next/image|assets|.*\\.(png|jpg|svg)$).*)", ], }; export function middleware(request: NextRequest) { if (hasSession(request)) { return NextResponse.next(); } const loginUrl = new URL("/login", request.url); return NextResponse.redirect(loginUrl); }
内容的提问来源于stack exchange,提问作者ODelibalta
相关产品推荐
相关产品推荐

