React登录跳转带用户_id专属页时_id为undefined异常排查
问题场景
基于React、React Router、Redux开发的前端应用,需要实现用户登录完成后自动重定向到携带用户自身_id的专属主页功能,核心实现代码如下:
根组件App.js代码
class App extends Component { componentDidMount() { store.dispatch(loadUser()); } render() { return ( <React.Fragment> <AppNavbar /> <div className="App"> <Switch> <Route path="/not-found" component={NotFound} /> <AuthRoute path="/profile/settings" type="private"> <Profile /> </AuthRoute> <AuthRoute path="/setup/school" type="private"> <SchoolDetails /> </AuthRoute> <AuthRoute path="/setup/team" type="private"> <TeamManagement /> </AuthRoute> <AuthRoute path="/create/teams" type="private"> <TeamSetup /> </AuthRoute> <AuthRoute path="/register/setup" type="guest"> <ProfileSetup /> </AuthRoute> <AuthRoute path="/events" type="private"> <SimpleSlider /> </AuthRoute> <AuthRoute path="/register" type="guest"> <RegisterModal /> </AuthRoute> <AuthRoute path="/login" type="guest"> <LoginModal /> </AuthRoute> <AuthRoute exact path="/" type="private"> <SimpleSlider /> </AuthRoute> <Redirect to="/not-found" /> </Switch> </div> </React.Fragment> ); } } const mapStateToProps = (state) => ({ isAuthenticated: state.auth.isAuthenticated, user: state.auth.user, }); export default connect(mapStateToProps)(App);
路由守卫组件AuthRoute.js代码
const AuthRoute = (props) => { const { isAuthenticated, user, type } = props; if (type == "guest" && isAuthenticated) { return <Redirect to={`/events/${user._id}`} />; } else if (type == "private" && !isAuthenticated) return <Redirect to={`/login`} />; return <Route {...props} />; }; const mapStateToProps = (state) => ({ isAuthenticated: state.auth.isAuthenticated, userloaded: state.auth.userloaded, user: state.auth.user, }); export default connect(mapStateToProps)(AuthRoute);
Redux认证模块reducer代码authReducer.js
const initialState = { token: localStorage.getItem("token"), isAuthenticated: null, userloaded: false, isLoading: false, user: null, }; export default function (state = initialState, action) { switch (action.type) { case USER_LOADING: return { ...state, isLoading: true, }; case USER_LOADED: return { ...state, user: action.payload, userloaded: true, isAuthenticated: true, isLoading: false, }; case LOGIN_SUCCESS: case REGISTER_SUCCESS: localStorage.setItem("token", action.payload.token); return { ...state, userloaded: true, isAuthenticated: true, isLoading: false, ...action.payload, }; case AUTH_ERROR: case LOGIN_FAIL: case LOGOUT_SUCCESS: case REGISTER_FAIL: localStorage.removeItem("token"); return { ...state, token: null, user: null, userloaded: false, isAuthenticated: false, isLoading: false, }; default: return state; } }
Redux认证模块action代码authActions.js
export const loadUser = () => (dispatch, getState) => { dispatch({ type: USER_LOADING }); axios .get("/api/auth/user", tokenConfig(getState)) .then((res) => dispatch({ type: USER_LOADED, payload: res.data, }) ) .catch((err) => { dispatch(returnErrors(err.response.data, err.response.status)); dispatch({ type: AUTH_ERROR }); }); }; export const register = ({ name, email, password }) => (dispatch) => { const config = { headers: { "Content-Type": "application/json", }, }; const body = JSON.stringify({ name, email, password }); axios .post("/api/users", body, config) .then((res) => dispatch({ type: REGISTER_SUCCESS, payload: res.data, }) ) .catch((err) => { dispatch( returnErrors(err.response.data, err.response.status, "REGISTER_FAIL") ); dispatch({ type: REGISTER_FAIL }); }); }; export const login = ({ email, password }) => (dispatch) => { const config = { headers: { "Content-Type": "application/json", }, }; const body = JSON.stringify({ email, password }); axios .post("/api/auth", body, config) .then((res) => dispatch({ type: LOGIN_SUCCESS, payload: res.data, }) ) .catch((err) => { dispatch( returnErrors(err.response.data, err.response.status, "LOGIN_FAIL") ); dispatch({ type: LOGIN_FAIL }); }); }; export const logout = () => { return { type: LOGOUT_SUCCESS, }; }; export const tokenConfig = (getState) => { const token = getState().auth.token; const config = { headers: { "Content-type": "application/json", }, }; if (token) { config.headers["x-auth-token"] = token; } return config; };
现存异常
- 异常1:用户登录后触发重定向逻辑时,Redux store中存储的user对象的
_id字段为undefined,页面会跳转到/events/undefined路径;只有手动刷新页面后,才能获取到正确的用户_id,跳转至正确的用户专属路径(例如/events/62a6503c9e8a60254ed94ff1)。 - 异常2:刷新应用页面时,登录页面会短暂闪现,之后才会加载对应正常页面。
根因定位
异常1根因
登录接口/api/auth返回的payload通常仅包含token字段,不返回完整用户信息(含_id),但当前LOGIN_SUCCESS的reducer逻辑直接展开整个payload存入store,还错误将userloaded标记为true,导致路由守卫判定用户信息已加载完成,此时user值为null,取user._id自然得到undefined。
刷新页面时componentDidMount会触发loadUser(),该接口会拉取完整用户信息存入store,因此刷新后能拿到正确的_id。
异常2根因
页面刷新时,loadUser()是异步请求,请求返回前isAuthenticated初始值为null,路由守卫逻辑仅判断!isAuthenticated就跳转登录页,没有区分「未登录」和「用户信息加载中」两种状态,导致加载过程中直接触发登录页重定向,等用户信息加载完成后又跳回正常页面,出现登录页短暂闪现问题。
修复方案
1. 调整authReducer逻辑
修改LOGIN_SUCCESS/REGISTER_SUCCESS处理规则:仅存储token,不提前标记userloaded为true,登录/注册成功后不直接写入user值,统一通过loadUser()拉取完整用户信息。
核心修改片段:
case LOGIN_SUCCESS: case REGISTER_SUCCESS: localStorage.setItem("token", action.payload.token); return { ...state, token: action.payload.token, isLoading: false, // 移除错误的userloaded: true和...action.payload展开逻辑 };
2. 补充login/register action逻辑
登录、注册接口请求成功、token存入store后,立即dispatchloadUser()拉取完整用户信息,保证后续重定向时能拿到包含_id的完整user对象。
login核心修改片段:
axios .post("/api/auth", body, config) .then((res) => { dispatch({ type: LOGIN_SUCCESS, payload: res.data, }); // 登录成功后立即拉取完整用户信息 dispatch(loadUser()); })
register action做相同修改,注册成功后追加dispatch(loadUser())即可。
3. 改造AuthRoute路由守卫
增加加载状态判断:当用户信息处于加载中(isLoading为true,或本地存在token但userloaded为false)时,渲染全局加载组件,不执行路由跳转,等用户信息加载完成后再走鉴权重定向逻辑,从根源避免登录页闪现、user为undefined时跳转的问题。
修改后的AuthRoute完整代码:
const AuthRoute = (props) => { const { isAuthenticated, user, type, isLoading, userloaded } = props; // 加载状态下渲染全局加载提示,不做跳转 if (isLoading || (localStorage.getItem("token") && !userloaded)) { return <div className="global-loading">页面加载中...</div>; } if (type == "guest" && isAuthenticated) { // 此时user已完成加载,可安全读取_id return <Redirect to={`/events/${user._id}`} />; } else if (type == "private" && !isAuthenticated) { return <Redirect to={`/login`} />; } return <Route {...props} />; }; const mapStateToProps = (state) => ({ isAuthenticated: state.auth.isAuthenticated, userloaded: state.auth.userloaded, isLoading: state.auth.isLoading, user: state.auth.user, }); export default connect(mapStateToProps)(AuthRoute);
4. 补充动态路由规则
当前路由表仅配置了/events路径,没有配置/events/:userId的动态路由,需要在App组件的Switch中补充对应规则,否则跳转到带id的路径后会命中404重定向:
<AuthRoute path="/events/:userId" type="private"> <SimpleSlider /> </AuthRoute>
修复效果
- 登录/注册成功后,会先拉取完整用户信息再触发重定向,直接跳转到携带正确用户
_id的专属主页,不会出现/events/undefined问题。 - 页面刷新时,加载状态下会展示全局加载提示,等用户信息校验完成后直接渲染目标页面,不会出现登录页短暂闪现问题。
内容的提问来源于stack exchange,提问作者Josua

