如何阻止React的路由私有路由在刷新时重定向到登录页面?

huangapple go评论94阅读模式
英文:

How can I stop react's router private route from redirecting to login page on refresh?

问题

以下是代码部分的翻译:

PrivateRoute.js

import { useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import Spinner from '../layout/Spinner';

const PrivateRoute = ({ children }) => {
    const { loading, isAuthenticated } = useSelector((state) => state.auth);

    if (loading) return <Spinner />
    if (!isAuthenticated) return <Navigate to='/login' />
    return children;
}

export default PrivateRoute;

App.js

import './App.css';
import 'react-phone-number-input/style.css';
import { useEffect } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { loadUser } from './reducers/authSlice';
import setAuthToken from './utils/setAuthToken';
import Products from './screens/Products';
import Product from './screens/Product';
import Navigator from './components/layout/Navigator';
import Footer from './components/layout/Footer';
import Signup from './screens/Signup';
import Login from './screens/Login';
import Dashboard from './screens/Dashboard';
import Settings from './screens/Settings';
import PrivateRoute from './components/routing/PrivateRoute';
import Store from './screens/Store';
import CreateOrUpdateStore from './screens/CreateOrUpdateStore';
import AddProduct from './screens/AddProduct';
import UpdateProduct from './screens/UpdateProduct';

const token = localStorage.getItem("token");
if (token) {
  setAuthToken(token); 
}

function App() {
  const dispatch = useDispatch();
  const { isAuthenticated } = useSelector((state) => state.auth);
  useEffect(() => {
    if (!isAuthenticated && token) {
      dispatch(loadUser());
    }
  }, []);

  return (
    <Router>
      <Navigator />
      <div className="container">
        <Routes>
          <Route path="/" element={<Products />} />
          <Route path="/products/:id" element={<Product />} />
          <Route path="/signup" element={<Signup />} />
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={<PrivateRoute><Dashboard /></PrivateRoute>} />
          <Route path="/settings" element={<PrivateRoute><Settings /></PrivateRoute>} />
          <Route path="/store" element={<PrivateRoute><Store /></PrivateRoute>} />
          <Route path="/create-store" element={<PrivateRoute><CreateOrUpdateStore /></PrivateRoute>} />
          <Route path="/update-store" element={<PrivateRoute><CreateOrUpdateStore /></PrivateRoute>} />
          <Route path="/add-product" element={<PrivateRoute ><AddProduct /></PrivateRoute>} />
          <Route path="/update-product/:id" element={<PrivateRoute><UpdateProduct /></PrivateRoute>} />
        </Routes>
      </div>
      <Footer />
    </Router>
  )
}

export default App;

Login.js

import { useState, useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import InputField from '../components/layout/InputField';
import Button from '../components/layout/Button';
import { faEnvelope, faUserCircle, faLock, faArrowRightToBracket } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { loginUser, clearMessages } from '../reducers/authSlice';
import FormAlert from '../components/layout/FormAlert';
import Spinner from '../components/layout/Spinner';

const Login = () => {
    const [formData, setFormData] = useState({
        email: '',
        password: ''
    });

    const { email, password } = formData;

    const dispatch = useDispatch();
    const { errors, message, loading, isAuthenticated } = useSelector((state) => state.auth);

    useEffect(() => {
        dispatch(clearMessages());
    },[dispatch]);

    const onChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    const getError = (name) => {
        const findError = errors.filter(error => error.param === name);
        if (findError.length > 0) {
            const error = errors.find(error => error.param === name);
            return error;
        }
    }

    const onSubmit = (e) => {
        e.preventDefault();
        const data = { email, password };
        dispatch(loginUser(data));
        setTimeout(() => {
            dispatch(clearMessages());
        }, 3000);
    }

    if (loading) return <Spinner />;

    if (isAuthenticated) return <Navigate to="/dashboard" />

    return (
        <form onSubmit={(e) => onSubmit(e)}>
            <FontAwesomeIcon style={{ backgroundColor: 'inherit', color: '#2596be', marginTop: '20px' }} size='8x' icon={faUserCircle} />
            {JSON.stringify(message) !== '{}' ? (<FormAlert alert={message} />) : ''}
            <InputField type='text' label='Email' name='email' value={email} changeHandler={onChange} error={getError('email')} icon={faEnvelope} />
            <InputField type='password' label='Password' name='password' value={password} changeHandler={onChange} error={getError('password')} icon={faLock} />
            <Button text='LOGIN' loading={loading} icon={faArrowRightToBracket} />
        </form>
    );
}

export default Login;

authSlice.js(这是 Redux 的一个 reducer 示例,内容较长,未全文翻译,如果需要请提供具体内容或问题)。

如果你有其他需要或有疑问,请随时告诉我。

英文:

PrivateRoute.js

import { useEffect } from &#39;react&#39;;
import { Navigate } from &#39;react-router-dom&#39;;
import { useDispatch, useSelector } from &#39;react-redux&#39;;
import Spinner from &#39;../layout/Spinner&#39;;
const PrivateRoute = ({ children }) =&gt; {
const { loading, isAuthenticated } = useSelector((state) =&gt; state.auth);
if (loading) return &lt;Spinner /&gt;
if (!isAuthenticated) return &lt;Navigate to=&#39;/login&#39; /&gt;
return children;
}
export default PrivateRoute;

App.js

import &#39;./App.css&#39;;
import &#39;react-phone-number-input/style.css&#39;
import { useEffect } from &#39;react&#39;;
import { BrowserRouter as Router, Route, Routes } from &#39;react-router-dom&#39;;
import { useDispatch, useSelector } from &#39;react-redux&#39;;
import { loadUser } from &#39;./reducers/authSlice&#39;;
import setAuthToken from &#39;./utils/setAuthToken&#39;;
import Products from &#39;./screens/Products&#39;;
import Product from &#39;./screens/Product&#39;;
import Navigator from &#39;./components/layout/Navigator&#39;;
import Footer from &#39;./components/layout/Footer&#39;;
import Signup from &#39;./screens/Signup&#39;;
import Login from &#39;./screens/Login&#39;;
import Dashboard from &#39;./screens/Dashboard&#39;;
import Settings from &#39;./screens/Settings&#39;;
import PrivateRoute from &#39;./components/routing/PrivateRoute&#39;;
import Store from &#39;./screens/Store&#39;;
import CreateOrUpdateStore from &#39;./screens/CreateOrUpdateStore&#39;;
import AddProduct from &#39;./screens/AddProduct&#39;;
import UpdateProduct from &#39;./screens/UpdateProduct&#39;;
const token = localStorage.getItem(&quot;token&quot;);
if (token) {
setAuthToken(token); 
}
function App() {
const dispatch = useDispatch();
const { isAuthenticated } = useSelector((state) =&gt; state.auth);
useEffect(() =&gt; {
if (!isAuthenticated &amp;&amp; token) {
dispatch(loadUser());
}
}, []);
return (
&lt;Router&gt;
&lt;Navigator /&gt;
&lt;div className=&quot;container&quot;&gt;
&lt;Routes&gt;
&lt;Route path=&quot;/&quot; element={&lt;Products /&gt;} /&gt;
&lt;Route path=&quot;/products/:id&quot; element={&lt;Product /&gt;} /&gt;
&lt;Route path=&quot;/signup&quot; element={&lt;Signup /&gt;} /&gt;
&lt;Route path=&quot;/login&quot; element={&lt;Login /&gt;} /&gt;
&lt;Route path=&quot;/dashboard&quot; element={&lt;PrivateRoute&gt;&lt;Dashboard /&gt;&lt;/PrivateRoute&gt;} /&gt;
&lt;Route path=&quot;/settings&quot; element={&lt;PrivateRoute&gt;&lt;Settings /&gt;&lt;/PrivateRoute&gt;} /&gt;
&lt;Route path=&quot;/store&quot; element={&lt;PrivateRoute&gt;&lt;Store /&gt;&lt;/PrivateRoute&gt;} /&gt;
&lt;Route path=&quot;/create-store&quot; element={&lt;PrivateRoute&gt;&lt;CreateOrUpdateStore /&gt;&lt;/PrivateRoute&gt;} /&gt;
&lt;Route path=&quot;/update-store&quot; element={&lt;PrivateRoute&gt;&lt;CreateOrUpdateStore /&gt;&lt;/PrivateRoute&gt;} /&gt;
&lt;Route path=&quot;/add-product&quot; element={&lt;PrivateRoute &gt;&lt;AddProduct /&gt;&lt;/PrivateRoute&gt;} /&gt;
&lt;Route path=&quot;/update-product/:id&quot; element={&lt;PrivateRoute&gt;&lt;UpdateProduct /&gt;&lt;/PrivateRoute&gt;} /&gt;
&lt;/Routes&gt;
&lt;/div&gt;
&lt;Footer /&gt;
&lt;/Router&gt;
)
}
export default App;

Login.js

import { useState, useEffect } from &#39;react&#39;;
import { Navigate } from &#39;react-router-dom&#39;;
import { useSelector, useDispatch } from &#39;react-redux&#39;;
import InputField from &#39;../components/layout/InputField&#39;;
import Button from &#39;../components/layout/Button&#39;;
import { faEnvelope, faUserCircle, faLock, faArrowRightToBracket } from &#39;@fortawesome/free-solid-svg-icons&#39;;
import { FontAwesomeIcon } from &#39;@fortawesome/react-fontawesome&#39;;
import { loginUser, clearMessages } from &#39;../reducers/authSlice&#39;;
import FormAlert from &#39;../components/layout/FormAlert&#39;;
import Spinner from &#39;../components/layout/Spinner&#39;;
const Login = () =&gt; {
const [ formData, setFormData ] = useState({
email:&#39;&#39;,
password:&#39;&#39;
});
const { email, password } = formData;
const dispatch = useDispatch();
const { errors, message, loading, isAuthenticated } = useSelector((state) =&gt; state.auth);
useEffect(() =&gt; {
dispatch(clearMessages());
},[dispatch]);
const onChange = (e) =&gt; {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const getError = (name) =&gt; {
const findError = errors.filter(error =&gt; error.param === name);
if (findError.length &gt; 0) {
const error = errors.find(error =&gt; error.param === name);
return error;
}
}
const onSubmit = (e) =&gt; {
e.preventDefault();
const data = { email, password };
dispatch(loginUser(data));
setTimeout(() =&gt; {
dispatch(clearMessages());
},3000);
}
if (loading) return &lt;Spinner /&gt;
if (isAuthenticated) return &lt;Navigate to=&quot;/dashboard&quot; /&gt;
return (
&lt;form onSubmit={(e) =&gt; onSubmit(e)}&gt;
&lt;FontAwesomeIcon style={{ backgroundColor: &#39;inherit&#39;, color: &#39;#2596be&#39;, marginTop: &#39;20px&#39; }} size=&#39;8x&#39; icon={faUserCircle} /&gt;
{JSON.stringify(message) !== &#39;{}&#39; ? (&lt;FormAlert alert={message} /&gt;) : &#39;&#39;}
&lt;InputField type=&#39;text&#39; label=&#39;Email&#39; name=&#39;email&#39; value={email} changeHandler={onChange} error={getError(&#39;email&#39;)} icon={faEnvelope} /&gt;
&lt;InputField type=&#39;password&#39; label=&#39;Password&#39; name=&#39;password&#39; value={password} changeHandler={onChange} error={getError(&#39;password&#39;)} icon={faLock} /&gt;
&lt;Button text=&#39;LOGIN&#39; loading={loading} icon={faArrowRightToBracket} /&gt;
&lt;/form&gt;
);
}
export default Login;

authSlice.js

import { createSlice, createAsyncThunk } from &#39;@reduxjs/toolkit&#39;;
import { authAPI } from &#39;../services/authAPI&#39;;
// Function for logging in user
export const loginUser = createAsyncThunk(
&quot;auth/loginUserStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.loginUser(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for loading user data
export const loadUser = createAsyncThunk(
&quot;auth/loadUserStatus&quot;,
async (_, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.loadUser();
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for signing up a user
export const signupUser = createAsyncThunk(
&quot;auth/signupUserStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.signupUser(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for verifying user accounts after sign up
export const verifyUser = createAsyncThunk(
&quot;auth/verifyUserStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.verifyUser(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for resending otp
export const resendOtp = createAsyncThunk(
&quot;auth/resendOtpStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.resendOtp(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for resending otp
export const retrievePassword = createAsyncThunk(
&quot;auth/retrievePasswordStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.retrievePassword(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for generating authentication token when reseting forgotten password
export const generateToken = createAsyncThunk(
&quot;auth/generateTokenStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.generateToken(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for creating new password
export const createNewPassword = createAsyncThunk(
&quot;auth/createNewPasswordStatus&quot;,
async (token, data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.createNewPassword(token, data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for updating user profile
export const updateAccount = createAsyncThunk(
&quot;auth/updateAccountStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.editAccount(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for changing the logged in user password
export const changePassword = createAsyncThunk(
&quot;auth/changePasswordStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.changePassword(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Function for uploading user profile picture
export const photoUpload = createAsyncThunk(
&quot;auth/uploadPhotoStatus&quot;,
async (data, { rejectWithValue }) =&gt; {
try {
const response = await authAPI.uploadPicture(data);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
)
// The initial state of the reducer
const initialState = {
token: localStorage.getItem(&quot;token&quot;),
isAuthenticated: false,
loading: false,
message: {},
errors: [],
user: null,
passwordResetToken: &#39;&#39;,
accountUpdating: false,
passwordChangeLoading: false,
photoUploadMessage: {},
photoUploadLoading: false
};
// The auth slice
export const authSlice = createSlice({
name: &quot;auth&quot;,
initialState,
reducers: {
clearMessages: (state) =&gt; {
state.message = {};
state.errors = [];
state.photoUploadMessage = {};
},
logout: (state) =&gt; {
state.token = localStorage.removeItem(&quot;token&quot;);
state.isAuthenticated = false;
state.user = null;
}
},
extraReducers: (builder) =&gt; {
builder.addCase(loginUser.pending, (state, { payload }) =&gt; {
state.user = null;
state.message = {};
state.errors = [];
state.loading = true;
});
builder.addCase(loginUser.fulfilled, (state, { payload }) =&gt; {
if (payload.status === &#39;created&#39;) {
state.message = payload;
} else {
localStorage.setItem(&quot;token&quot;, payload.data);
}
state.loading = false;
state.isAuthenticated = true;
});
builder.addCase(loginUser.rejected, (state, { payload }) =&gt; {
if (payload.errors.length === 1) {
state.message = payload.errors[0];
} else {
state.errors = payload.errors;
}
state.loading = false;
state.isAuthenticated = false;
});
builder.addCase(loadUser.pending, (state, { payload }) =&gt; {
state.errors = [];
state.message = {};
state.loading = true;
state.isAuthenticated = false;
});
builder.addCase(loadUser.fulfilled, (state, { payload }) =&gt; {
state.user = payload.data;
state.isAuthenticated = true;
state.loading = false;
});
builder.addCase(loadUser.rejected, (state, { payload }) =&gt; {
state.loading = false;
state.message = payload;
state.user = null;
});
builder.addCase(signupUser.pending, (state, { payload }) =&gt; {
state.message = {};
state.user = null;
state.errors = [];
state.loading = true;
});
builder.addCase(signupUser.fulfilled, (state, { payload }) =&gt; {
state.loading = false;
state.message = payload;
state.user = { phonenumber: payload.data };
});
builder.addCase(signupUser.rejected, (state, { payload }) =&gt; {
state.loading = false;
if (payload.errors.length === 1) {
state.message = payload.errors[0];
} else {
state.errors = payload.errors;
}
});
builder.addCase(verifyUser.pending, (state, { payload }) =&gt; {
state.message = {};
state.errors = [];
state.loading = true;
});
builder.addCase(verifyUser.fulfilled, (state, { payload }) =&gt; {
state.user = null;
state.message = payload;
state.loading = false;
});
builder.addCase(verifyUser.rejected, (state, { payload }) =&gt; {
state.message = payload.errors[0];
state.loading = false;
});
builder.addCase(resendOtp.pending, (state, { payload }) =&gt; {
state.message = {};
state.loading = true;
state.errors = [];
});
builder.addCase(resendOtp.fulfilled, (state, { payload }) =&gt; {
state.loading = false;
state.message = payload;
});
builder.addCase(resendOtp.rejected, (state, { payload }) =&gt; {
state.loading = false;
state.message = payload.errors[0];
});
builder.addCase(retrievePassword.pending, (state, { payload }) =&gt; {
state.message = {};
state.loading = true;
state.errors = [];
});
builder.addCase(retrievePassword.fulfilled, (state, { payload }) =&gt; {
state.loading = false;
state.message = payload;
});
builder.addCase(retrievePassword.rejected, (state, { payload }) =&gt; {
state.loading = false;
state.errors = payload.errors;
});
builder.addCase(generateToken.pending, (state, { payload }) =&gt; {
state.message = {};
state.passwordResetToken = &#39;&#39;;
state.loading = true;
state.errors = [];
});
builder.addCase(generateToken.fulfilled, (state, { payload }) =&gt; {
state.loading = false;
state.passwordResetToken = payload.data;
});
builder.addCase(generateToken.rejected, (state, { payload }) =&gt; {
state.loading = false;
state.errors = payload.errors;
});
builder.addCase(createNewPassword.pending, (state, { payload }) =&gt; {
state.message = {};
state.loading = true;
state.errors = [];
});
builder.addCase(createNewPassword.fulfilled, (state, { payload }) =&gt; {
state.loading = false;
state.message = payload;
});
builder.addCase(createNewPassword.rejected, (state, { payload }) =&gt; {
state.loading = false;
state.errors.push(payload);
});
builder.addCase(updateAccount.pending, (state, { payload }) =&gt; {
state.accountUpdating = true;
});
builder.addCase(updateAccount.fulfilled, (state, { payload }) =&gt; {
state.accountUpdating = false;
state.user = payload.data;
const { data, ...rest } = payload;
state.message = rest;
});
builder.addCase(updateAccount.rejected, (state, { payload }) =&gt; {
state.accountUpdating = false;
if (payload.errors &amp;&amp; payload.errors.length &gt; 0) {
state.errors = payload.errors;
} else {
state.message = payload;
}
});
builder.addCase(changePassword.pending, (state, { payload }) =&gt; {
state.passwordChangeLoading = true;
});
builder.addCase(changePassword.fulfilled, (state, { payload }) =&gt; {
state.passwordChangeLoading = false;
state.message = payload;
});
builder.addCase(changePassword.rejected, (state, { payload }) =&gt; {
state.passwordChangeLoading = false;
if (payload.errors) {
state.errors = payload.errors;
} else {
state.message = payload;
}
});
builder.addCase(photoUpload.pending, (state, { payload }) =&gt; {
state.photoUploadLoading = true;
});
builder.addCase(photoUpload.fulfilled, (state, { payload }) =&gt; {
state.photoUploadLoading = false;
const { data, ...rest } = payload;
state.photoUploadMessage = rest;
state.user = data;
});
builder.addCase(photoUpload.rejected, (state, { payload }) =&gt; {
state.photoUploadLoading = false;
state.photoUploadMessage = payload;
});
}
});
export const { clearMessages, logout } = authSlice.actions;
export default authSlice.reducer;

I tried creating a private route and preventing the loadUser action from running on refresh if the user is already logged in by creating a condition on the App.js; but this method still prove to be ineffective in solving the problem.

答案1

得分: 2

使用一个最初为真的 loading 状态,以便在 PrivateRoute 组件检查 isAuthenticated 状态并重定向之前,App 中的 useEffect 有机会运行并检查身份验证状态。您还可以初始化 isAuthenticated 为 true,如果本地存储中有持久化的令牌。

authSlice.js

const initialState = {
  token: localStorage.getItem("token"),
  isAuthenticated: !!localStorage.getItem("token"),
  loading: true,
  message: {},
  errors: [],
  user: null,
  passwordResetToken: '',
  accountUpdating: false,
  passwordChangeLoading: false,
  photoUploadMessage: {},
  photoUploadLoading: false
};
const PrivateRoute = ({ children }) => {
  const { loading, isAuthenticated } = useSelector((state) => state.auth);
  
  if (loading) return <Spinner />;
  if (!isAuthenticated) return <Navigate to='/login' />;
  return children;
}
英文:

Use a loading state that is initially true so that the useEffect in App has a chance to run and check the auth status prior to the PrivateRoute component checking the isAuthenticated state and redirecting. You may also want to initialize isAuthenticated to true of there was a persisted token in localStorage.

authSlice.js

const initialState = {
  token: localStorage.getItem(&quot;token&quot;),
  isAuthenticated: !!localStorage.getItem(&quot;token&quot;),
  loading: true,
  message: {},
  errors: [],
  user: null,
  passwordResetToken: &#39;&#39;,
  accountUpdating: false,
  passwordChangeLoading: false,
  photoUploadMessage: {},
  photoUploadLoading: false
};
const PrivateRoute = ({ children }) =&gt; {
const { loading, isAuthenticated } = useSelector((state) =&gt; state.auth);
if (loading) return &lt;Spinner /&gt;;
if (!isAuthenticated) return &lt;Navigate to=&#39;/login&#39; /&gt;;
return children;
}

huangapple
  • 本文由 发表于 2023年2月14日 07:29:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75442121.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定