React Router调用navigate('/')后组件未渲染,URL更新但页面未切换
登录成功后调用navigate('/'),URL从/get-started/signup变为/,导航栏登录按钮替换为用户头像,但<Home/>组件未渲染,<SignUp/>组件也未卸载。
相关代码片段
组件内登录处理函数
const { isAuthenticating, error, handleAuth } = useAuth() const handleSignIn = async (e) => { e.preventDefault() const success = await handleAuth(signin, emailRef.current.value, passwordRef.current.value) if (success) { navigate('/') } }
useAuth.js中的handleAuth实现
const { isAuthenticating, error } = useSelector(state => state.user) const handleAuth = async (authType, email, password) => { try { const action = await dispatch(authType({ email, password })) if (authType.fulfilled.match(action)) { console.log('worked') return true; } } catch (err) { console.log(err) return false; } }
signin异步Thunk
export const signin = createAsyncThunk( 'user/signin', async({ email, password }, { rejectWithValue }) => { try { const response = await signInWithEmailAndPassword(auth, email, password) const accessToken = await response.user.getIdToken() return { uid: response.user.uid, email: response.user.email, accessToken } } catch (error) { return rejectWithValue(error.message) } } )
Redux User Slice配置
初始状态
const initialState = { user: null, error: { type: null, message: null }, isAuthenticating: false, }
Extra Reducers处理
extraReducers: (builder) => { builder .addCase(signin.pending, (state) => { state.isAuthenticating = true; state.error = initialState.error; }) .addCase(signin.fulfilled, (state, action) => { state.user = action.payload; state.isAuthenticating = false; localStorage.setItem('accessToken', action.payload.accessToken); localStorage.setItem('email', action.payload.email); localStorage.setItem('uid', action.payload.uid); }) .addCase(signin.rejected, (state, action) => { state.isAuthenticating = false; state.error = handleError(action.payload); }); },
路由与私有路由配置
路由定义
const router = createBrowserRouter([ { path: '/', element: <RootLayout />, errorElement: <Error />, children: [ { index: true, element: <PrivateRoute element={<Home />} />, }, { path: 'get-started', element: <LandingLayout />, children: [ { index: true, element: <GetStarted />, }, { path: 'signup', element: <SignUp /> }, ] } ] } ])
PrivateRoute组件(原错误版本)
const PrivateRoute = ({ element }) => { const user = useSelector(state => state.user) return user ? element : <Navigate to="/get-started" /> }
RootLayout组件
const RootLayout = () => { return ( <> <Nav /> <Outlet /> </> ) }
问题原因分析
核心问题出在**PrivateRoute的状态判断逻辑错误**:
useSelector(state => state.user)获取的是整个userslice对象(包含user、error、isAuthenticating属性),而非实际的用户信息字段state.user.user。- 由于slice对象始终是一个非空对象,
return user ? element : ...的判断永远为真,导致PrivateRoute无论用户是否登录都会返回<Home/>组件。但登录后路由切换时,Redux状态更新的时机与路由渲染的时机不匹配,加上路由层级的嵌套关系,最终出现URL更新但组件未正确切换的异常。
解决方案
1. 修正PrivateRoute的状态判断逻辑
将PrivateRoute中获取用户状态的代码改为实际用户信息字段,并添加replace属性优化重定向行为:
const PrivateRoute = ({ element }) => { // 正确获取store中的用户信息 const user = useSelector(state => state.user.user) // 用户存在则渲染目标组件,否则重定向到登录页面 return user ? element : <Navigate to="/get-started" replace /> }
2. 补充初始化时的用户状态恢复逻辑
为避免刷新页面后用户状态丢失,在user-slice的初始状态中从localStorage读取已保存的用户数据:
const initialState = { // 从localStorage恢复用户状态 user: localStorage.getItem('uid') ? { uid: localStorage.getItem('uid'), email: localStorage.getItem('email'), accessToken: localStorage.getItem('accessToken') } : null, error: { type: null, message: null }, isAuthenticating: false, }
3. 验证路由切换逻辑
确保handleSignIn中navigate('/')的调用是在Redux状态更新完成后执行的(当前代码通过await dispatch(authType(...))已实现这一点,无需修改)。
完成以上修改后,登录成功调用navigate('/')时,PrivateRoute会正确识别用户已登录,渲染<Home/>组件,同时卸载<SignUp/>组件,路由切换功能完全生效。
内容的提问来源于stack exchange,提问作者Zainab Hussain
相关产品推荐
相关产品推荐

