React Router与Redux认证令牌引发组件重复渲染问题排查
页面刷新时Redux Token先null后有效值,App组件渲染两次的原因及优化方案
我在项目中使用Redux实现认证逻辑:用户登录时派发令牌,随后获取用户令牌,根据令牌是否存在展示不同路由(存在时展示X路由,不存在时展示Y路由)。但遇到问题:每次页面刷新时,App组件都会渲染两次,第一次token值为null,第二次才变为有效值。想请教该现象的原因,同时希望得到更高效的实现方案。
相关代码:
const App = () => { const dispatch = useDispatch(); const token = useSelector((state) => state.auth.token); const onCheckAuth = useCallback(() => dispatch(checkAuth()), [dispatch]); useEffect(() => { onCheckAuth(); }, [onCheckAuth]); const render = () => { if (token) { return ( <Switch> <Route path='/hearing' component={Hearing} /> <Route path='/logout' component={Logout} /> <Route path='/' exact component={Dashboard} /> <Redirect to='/' /> </Switch> ); } return ( <Switch> <Route path='/login' component={Login} /> <Redirect to='/login' /> </Switch> ); }; console.log(token); return <> {render()} </>; }; export default App;
为什么会出现两次渲染?
- 初始Redux状态的默认值:你的Redux
auth模块初始状态里,token应该是null对吧?页面刷新后,Redux store会重置为初始状态,所以第一次渲染时useSelector拿到的就是这个初始的null值。 useEffect的异步执行特性:useEffect会在组件首次挂载完成后才触发onCheckAuth(也就是dispatch(checkAuth()))。这个checkAuthaction应该是从localStorage或其他持久化存储读取token、并更新store的操作,它是异步执行的——等action完成后store里的token更新,useSelector检测到状态变化,就会触发组件第二次渲染,这时候就能拿到有效值了。
更高效的实现方案
1. 提前初始化Redux状态,避免首次null
不要等useEffect触发checkAuth去获取token,而是在Redux store初始化时,直接从持久化存储(比如localStorage)读取token并设置初始状态。这样组件第一次渲染就能拿到正确的token值,减少一次不必要的渲染。
假设你用Redux Toolkit编写auth模块,示例代码如下:
import { createSlice } from '@reduxjs/toolkit'; // 初始化时直接从localStorage读取token const initialState = { token: localStorage.getItem('token') || null, // 其他状态字段... }; const authSlice = createSlice({ name: 'auth', initialState, reducers: { setToken: (state, action) => { state.token = action.payload; localStorage.setItem('token', action.payload); }, logout: (state) => { state.token = null; localStorage.removeItem('token'); } } }); export const { setToken, logout } = authSlice.actions; export default authSlice.reducer;
2. 添加加载状态,避免路由闪烁
如果你的checkAuth需要调用后端接口验证token有效性(异步操作),那必须添加加载状态,避免用户看到短暂的登录页面再跳转到主页。可以在auth状态里新增isLoading字段:
// auth模块初始状态 const initialState = { token: localStorage.getItem('token') || null, isLoading: true, // 初始标记为加载中 }; // 用createAsyncThunk编写异步验证逻辑 export const checkAuth = createAsyncThunk( 'auth/checkAuth', async (_, { dispatch }) => { const token = localStorage.getItem('token'); if (token) { // 调用后端接口验证token有效性 const response = await fetch('/api/verify-token', { headers: { Authorization: `Bearer ${token}` } }); if (!response.ok) { // token无效,清除状态 dispatch(logout()); } } // 加载完成,更新状态 dispatch(setIsLoading(false)); } ); // 在slice中处理异步action的状态变化 extraReducers: (builder) => { builder .addCase(checkAuth.pending, (state) => { state.isLoading = true; }) .addCase(checkAuth.fulfilled, (state) => { state.isLoading = false; }) .addCase(checkAuth.rejected, (state) => { state.isLoading = false; state.token = null; }); }
然后在App组件中处理加载状态:
const App = () => { const dispatch = useDispatch(); const { token, isLoading } = useSelector((state) => state.auth); useEffect(() => { dispatch(checkAuth()); }, [dispatch]); // 加载中时展示加载组件,避免路由闪烁 if (isLoading) { return <div>Loading...</div>; } return ( <Switch> {token ? ( // 已认证路由 <> <Route path='/hearing' component={Hearing} /> <Route path='/logout' component={Logout} /> <Route path='/' exact component={Dashboard} /> <Redirect to='/' /> </> ) : ( // 未认证路由 <> <Route path='/login' component={Login} /> <Redirect to='/login' /> </> )} </Switch> ); }; export default App;
3. 封装Protected Route组件,简化路由逻辑
把需要认证的路由封装成单独的组件,让代码更清晰,避免在App组件中写大量判断逻辑:
// ProtectedRoute.jsx import { Route, Redirect } from 'react-router-dom'; import { useSelector } from 'react-redux'; const ProtectedRoute = ({ component: Component, ...rest }) => { const { token, isLoading } = useSelector((state) => state.auth); if (isLoading) { return <div>Loading...</div>; } return ( <Route {...rest} render={(props) => token ? <Component {...props} /> : <Redirect to="/login" /> } /> ); }; export default ProtectedRoute;
在App组件中使用:
const App = () => { const dispatch = useDispatch(); const { isLoading, token } = useSelector((state) => state.auth); useEffect(() => { dispatch(checkAuth()); }, [dispatch]); if (isLoading) { return <div>Loading...</div>; } return ( <Switch> {/* 受保护的路由 */} <ProtectedRoute path='/hearing' component={Hearing} /> <ProtectedRoute path='/logout' component={Logout} /> <ProtectedRoute path='/' exact component={Dashboard} /> {/* 未认证路由 */} {!token && <Route path='/login' component={Login} />} {/* 全局重定向 */} <Redirect to={token ? '/' : '/login'} /> </Switch> ); }; export default App;
内容的提问来源于stack exchange,提问作者nishi
相关产品推荐
相关产品推荐

