用户注册后token未存入localStorage导致无法自动登录问题
注册表单提交后,用户已成功创建,但token未被存入localStorage,无法自动跳转到应用内部,必须手动登录。测试确认registerUser动作已获取到有效token(Postman调用/me端点验证正确),但注册完成后loadUser动作被拒绝,localStorage为空。
相关代码
RegisterForm.jsx
import React, { useState, useEffect} from 'react' import { useNavigate } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; import { registerUser } from '../../slices/authSlice'; import FormGroup from '../UI/FormGroup'; import '../../styles/UI/FormGroup.css' function RegisterForm() { const dispatch = useDispatch(); const { isAuthenticated, error } = useSelector(state => state.auth) const navigate = useNavigate(); const [formData, setFormData] = useState({ fullname: "", birth_date: "", gender: "", email: "", password: "", password2: "", }); const onChange = (e) => { const { name, value, type, checked } = e.target; if (type === "checkbox") { if (checked) { setFormData({ ...formData, [name]: [...gender, value] }); } else { setFormData({ ...formData, [name]: gender.filter((option) => option !== value), }); } } else { setFormData({ ...formData, [name]: value }); } } const formFields = [ { name: "fullname", type: "text", placeholder: "Insert Full Name", label: "Full Name", value: formData.fullname, onChange: onChange }, { name: "gender", type: "radio", placeholder: "", label: "", value: formData.gender, onChange: onChange, options: ["male", "female"] }, { name: "birth_date", type: "date", placeholder: "", label: "Date of Birth", value: formData.birth_date, onChange: onChange }, { name: "email", type: "email", placeholder: "Email", label: "Email", value: formData.email, onChange: onChange }, { name: "password", type: "password", placeholder: "Password", label: "Password", value: formData.password, onChange: onChange }, { name: "password2", type: "password", placeholder: "Confirm Password", label: "Confirm Password", value: formData.password2, onChange: onChange } ] const onSubmitHandler = (e) => { e.preventDefault(); dispatch(registerUser(formData)); }; useEffect(() => { if (isAuthenticated) { navigate('/'); } }, [isAuthenticated, navigate]); return ( <form action="" className="form_container" onSubmit={(e) => onSubmitHandler(e)}> {formFields.map((field) => ( <FormGroup key={field.name} {...field} /> ))} <button type="submit">Register</button> {error && <p style={{ color: 'red', marginTop: '10px' }} className="error">{error.message}</p>} </form> ) } export default RegisterForm
authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; import setAuthToken from '../utils/setAuthToken'; const initialState = { access_token: localStorage.getItem('token'), isAuthenticated: false, user: {}, isLoading: false, error: null, }; export const loadUser = createAsyncThunk( 'auth/loadUser', async (_, { rejectWithValue }) => { if (localStorage.token) { setAuthToken(localStorage.token); } try { const res = await axios.get('http://localhost:8000/api/auth/me'); return res.data; } catch (error) { return rejectWithValue(error.response.data); } } ); export const registerUser = createAsyncThunk( 'auth/registerUser', async (user, { dispatch, rejectWithValue }) => { try { const config = { headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }; const body = JSON.stringify(user); const res = await axios.post( 'http://localhost:8000/api/auth/register', body, config ); // 先将token存入localStorage,再调用loadUser localStorage.setItem('token', res.data.access_token); dispatch(loadUser()); return res.data; } catch (error) { return rejectWithValue(error.response.data.message); } } ); export const loginUser = createAsyncThunk( 'auth/loginUser', async ({ email, password }, { dispatch, rejectWithValue }) => { const body = { email, password }; const config = { headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }; try { const res = await axios.post( 'http://localhost:8000/api/auth/login', body, config ); localStorage.setItem('token', res.data.access_token); dispatch(loadUser()); return res.data; } catch (error) { return rejectWithValue(error.response.data.message); } } ); export const authSlice = createSlice({ name: 'auth', initialState, reducers: { logoutUser: (state) => { state.access_token = null; localStorage.removeItem('token'); state.isAuthenticated = false; state.user = null; }, }, extraReducers: (builder) => { builder .addCase(loadUser.pending, (state) => { state.isLoading = true; state.isAuthenticated = state.access_token ? true : false; }) .addCase(loadUser.fulfilled, (state, action) => { state.isLoading = false; state.isAuthenticated = true; state.user = action.payload; state.error = null; }) .addCase(loadUser.rejected, (state, action) => { state.isLoading = false; state.isAuthenticated = false; state.error = action.payload; }) .addCase(registerUser.pending, (state) => { state.isLoading = true; }) .addCase(registerUser.fulfilled, (state, action) => { // 直接赋值token,不要用localStorage.setItem的返回值(返回undefined) state.access_token = action.payload.access_token; state.isLoading = false; state.isAuthenticated = true; state.user = action.payload; state.error = null; }) .addCase(registerUser.rejected, (state, action) => { state.isLoading = false; state.user = null; state.isAuthenticated = false; state.error = action.payload; }) .addCase(loginUser.pending, (state) => { state.isLoading = true; }) .addCase(loginUser.fulfilled, (state, action) => { state.access_token = action.payload.access_token; state.isLoading = false; state.isAuthenticated = true; console.log(state.isAuthenticated); state.user = action.payload; state.error = null; }) .addCase(loginUser.rejected, (state, action) => { state.isLoading = false; state.user = null; state.isAuthenticated = false; state.error = action.payload; }); }, }); export const { logoutUser } = authSlice.actions; export default authSlice.reducer;
App.jsx
import './App.css'; import Home from './components/UI/Home'; import { Route, Routes } from 'react-router-dom'; import Navbar from './components/UI/Navbar'; import Footer from './components/UI/Footer'; import Register from './Pages/Register'; import Login from './Pages/Login'; import Profile from './Pages/Profile'; import Dashboard from './Pages/Dashboard'; import PrivateRoute from './components/Routing/PrivateRoute'; import useAuth from './customHooks/useAuth'; function App() { const { isAuthenticated, loading } = useAuth(); if (loading) { return <h1 className='title loader'>Loading...</h1>; } return ( <> <div className='content-container'> <Navbar /> <Routes> <Route path='/login' element={<Login />} /> <Route path='/profile' element={<PrivateRoute><Profile /></PrivateRoute>} /> <Route path='/register' element={<Register />} /> <Route path="/" element={isAuthenticated ? <Dashboard /> : <Home />} /> </Routes> <Footer /> </div> </> ); } export default App;
useAuth.js
import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { loadUser } from '../slices/authSlice'; const useAuth = () => { const { isAuthenticated } = useSelector((state) => state.auth); const dispatch = useDispatch(); const [loading, setLoading] = useState(true); useEffect(() => { const loadAuthenticatedUser = async () => { // loadUser不需要传参,内部会从localStorage读取 await dispatch(loadUser()); setLoading(false); }; loadAuthenticatedUser(); }, [dispatch]); return { isAuthenticated, loading }; }; export default useAuth;
问题分析与修复方案
核心问题1:Token存入时机错误
原registerUser thunk中,先调用dispatch(loadUser(res.data))再存token到localStorage,导致loadUser执行时localStorage无token,setAuthToken无法设置请求头,/api/auth/me请求失败。
修复:在registerUser thunk中,拿到后端返回的token后,先执行localStorage.setItem('token', res.data.access_token),再调用dispatch(loadUser()),确保loadUser执行时localStorage已有token。
核心问题2:state.access_token赋值错误
原registerUser.fulfilled中,用state.access_token = localStorage.setItem(...),但localStorage.setItem返回undefined,导致state.access_token被设为无效值。
修复:直接将state.access_token赋值为action.payload.access_token,存token的操作已在thunk中完成。
次要问题:loadUser参数冗余
loadUser thunk未使用传入的参数,useAuth中无需传参,直接调用dispatch(loadUser())即可。
修复后流程
- 用户提交注册表单,
registerUserthunk发起请求 - 后端返回用户信息和token,thunk将token存入localStorage
- 调用
loadUser,此时localStorage已有token,setAuthToken正确设置请求头 loadUser成功获取用户信息,更新redux状态,isAuthenticated变为trueRegisterForm中的useEffect监听isAuthenticated变化,触发跳转至首页
内容的提问来源于stack exchange,提问作者tukitooo1998

