You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()))。这个checkAuth action应该是从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 12:33:15