使用Firebase与Redux在Next.js的_app.js中做访问控制时的页面闪现问题
问题原因及解决方案
问题描述
我开发了一个应用,未登录用户可访问网站,但仅已认证用户能访问/app、/app/*路径。现有代码功能正常,但未登录访问/app时,会先短暂显示页面内容,再提示“您无权访问该页面”。请问这是什么原因?
用户提供的原始代码:
import { Provider, useDispatch, useSelector } from 'react-redux'; import { useRouter } from 'next/router'; import '../styles/front.css'; import '../styles/app.css'; import React, { useEffect, useState } from 'react'; import { wrapper, store } from '../store'; import { login, logout, selectUser } from "../redux/slices/userSlice"; import { auth } from '../firebase'; function MyApp({ Component, pageProps }) { const user = useSelector(selectUser); const dispatch = useDispatch(); const router = useRouter(); const isAppPage = router.pathname.startsWith('/app'); const [shouldRender, setShouldRender] = useState(true); // New state variable const [loading, setLoading] = useState(true); const [isAuthenticated, setIsAuthenticated] = useState(false); useEffect(() => { const unsubscribe = auth.onAuthStateChanged((userAuth) => { if (userAuth) { dispatch(login({ email: userAuth.email, uid: userAuth.uid, displayName: userAuth.displayName, photoUrl: userAuth.photoURL })); setIsAuthenticated(true); } else { if (isAppPage) { setShouldRender(false); } dispatch(logout()); setIsAuthenticated(false); } setLoading(false); // Set loading to false once the authentication status is checked }); return () => unsubscribe(); // Cleanup the event listener when the component unmounts }, []); return ( <Provider store={store}> {shouldRender ? <Component {...pageProps} /> : <p>You're not allowed to access that page.</p>} </Provider> ); } export default wrapper.withRedux(MyApp);
核心原因
- 初始状态默认允许渲染:
shouldRender初始值设为true,而Firebase的auth.onAuthStateChanged是异步执行的——组件挂载后会先渲染目标页面内容,等Firebase返回认证状态结果后,才会修改shouldRender为false,这就导致了短暂的未授权内容泄露。 - 依赖项缺失:
useEffect的依赖数组为空,但内部用到了isAppPage(依赖router.pathname),当路由切换时,逻辑不会重新计算,可能导致判断失效。 - 状态冗余:已经通过Redux的
selectUser获取用户状态,单独维护isAuthenticated状态容易出现状态不一致问题。
修复后的代码
import { Provider, useDispatch, useSelector } from 'react-redux'; import { useRouter } from 'next/router'; import '../styles/front.css'; import '../styles/app.css'; import React, { useEffect, useState } from 'react'; import { wrapper, store } from '../store'; import { login, logout, selectUser } from "../redux/slices/userSlice"; import { auth } from '../firebase'; function MyApp({ Component, pageProps }) { const user = useSelector(selectUser); const dispatch = useDispatch(); const router = useRouter(); const isAppPage = router.pathname.startsWith('/app'); const [loading, setLoading] = useState(true); useEffect(() => { const unsubscribe = auth.onAuthStateChanged((userAuth) => { if (userAuth) { dispatch(login({ email: userAuth.email, uid: userAuth.uid, displayName: userAuth.displayName, photoUrl: userAuth.photoURL })); } else { dispatch(logout()); // 未登录访问app页面时,可选择跳转首页或显示提示 if (isAppPage) { router.push('/'); } } setLoading(false); }); return () => unsubscribe(); }, [dispatch, isAppPage, router]); // 加载状态下显示加载内容,避免提前渲染未授权页面 if (loading) { return <div>加载中...</div>; // 可替换为自定义加载组件 } // 未登录访问app页面时,显示权限提示 if (isAppPage && !user) { return <p>您无权访问该页面。</p>; } return ( <Provider store={store}> <Component {...pageProps} /> </Provider> ); } export default wrapper.withRedux(MyApp);
关键修改说明
- 利用加载状态阻塞渲染:在认证状态未确定前,只显示加载内容,彻底避免未授权内容提前显示。
- 修复依赖项:将
dispatch、isAppPage、router加入useEffect依赖数组,确保路由切换时逻辑能重新执行。 - 简化状态判断:直接使用Redux中的
user状态判断认证状态,去掉冗余状态,减少不一致风险。 - 可选体验优化:未登录访问/app时,可选择直接跳转到首页,替代静态提示文本(可根据业务需求调整)。
内容的提问来源于stack exchange,提问作者Brian Millot
相关产品推荐
相关产品推荐

