I have an issue when use navigate
from login page to "/dashboard"
. When user click to login, they correctly redirect to "/dashboard"
but show me a blank page, but when refresh the page everything works correctly.
authSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import api from "../../services/api.js";
export const login = createAsyncThunk(
"auth/login",
async (payload) => {
try {
const response = await api.request({
url: `/api/account/token/portal/organic`,
method: "post",
data: payload,
});
return response.data.result;
} catch (error) {
console.error("error", error);
return Promise.reject(error);
}
});
const authSlice = createSlice({
name: "auth",
initialState: {
token: JSON.parse(localStorage.getItem("token")) || {},
isLoading: false,
hasError: null
},
reducers: {
logout: (state) => {
state.token = {};
localStorage.removeItem("token");
}
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.isLoading = true;
state.hasError = null;
})
.addCase(login.fulfilled, (state, action) => {
state.token = action.payload;
state.isLoading = false;
state.hasError = null
localStorage.setItem("token", JSON.stringify(action.payload));
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false;
state.hasError = action.error;
})
}
});
// Selectors
export const selectToken = state => state.auth.token;
export const selectLoadingState = state => state.auth.isLoading;
export const selectErrorState = state => state.auth.hasError;
export const { logout } = authSlice.actions;
export default authSlice.reducer;
DashboardLayout.jsx
import { Navigate, Outlet } from "react-router-dom";
import {useDispatch, useSelector} from "react-redux";
import { selectToken } from "../store/auth/authSlice.js";
import { logout } from "../store/auth/authSlice.js";
const DashboardLayout = () => {
const { accessToken } = useSelector(selectToken);
const dispatch = useDispatch();
const handleLogout = () => {
dispatch(logout());
};
if (!accessToken) {
return <Navigate to="/login" />;
}
return (
<>
<Outlet />
<button onClick={handleLogout}>
Logout
</button>
</>
)
}
export default DashboardLayout
LoginPage.jsx
import React, { useEffect, useState } from 'react';
import { Form, Input, Button, Card, Alert, Layout } from 'antd';
import { useDispatch, useSelector } from "react-redux";
import { login, selectErrorState, selectToken } from "../store/auth/authSlice.js";
import { useNavigate } from "react-router-dom";
const LoginPage = () => {
const [form] = Form.useForm();
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState('');
const error = useSelector(selectErrorState);
const navigate = useNavigate()
const onFinish = (values) => {
dispatch(login(values))
.then(() => {
navigate('/dashboard');
})
.catch((error) => {
console.log('Login failed:', error);
});
};
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo);
};
return (
<Layout
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
<Card
title="Login"
style={{
width: 480,
}}
>
{error && (
<Alert message={error.message} type="error" showIcon closable />
)}
<Form
form={form}
name="basic"
labelCol={{
span: 6,
}}
wrapperCol={{
span: 17,
}}
initialValues={{
remember: true,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label="Username"
name="Username"
rules={[
{
required: true,
message: 'Please input your username!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="Password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item
wrapperCol={{
offset: 8,
span: 16,
}}
>
<Button type="primary" htmlType="submit">
Login
</Button>
</Form.Item>
</Form>
</Card>
</Layout>
);
};
export default LoginPage;
Route.js
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectToken } from "../store/auth/authSlice.js";
import DashboardLayout from "../template/DashboardLayout.jsx";
import LoginPage from "../pages/Login.jsx";
const Routes = () => {
const { accessToken } = useSelector(selectToken);
// Define public routes accessible to all users
const routesForPublic = [
{
path: "/service",
element: <div>Service Page</div>,
},
{
path: "/about-us",
element: <div>About Us</div>,
},
];
// Define routes accessible only to authenticated users
const routesForAuthenticatedOnly = [
{
path: "/dashboard",
element: <DashboardLayout />, // Wrap the component in ProtectedRoute
children: [
{
path: "",
element: <h1>Dashboard</h1>,
},
],
},
];
// Define routes accessible only to non-authenticated users
const routesForNotAuthenticatedOnly = [
{
path: "/",
element: <div>Home Page</div>,
},
{
path: "/login",
element: <LoginPage />,
},
];
// Combine and conditionally include routes based on authentication status
const router = createBrowserRouter([
...routesForPublic,
...(!accessToken ? routesForNotAuthenticatedOnly : []),
...routesForAuthenticatedOnly,
]);
// Provide the router configuration using RouterProvider
return <RouterProvider router={router} />;
};
export default Routes;
store.js
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import authReducer from "./auth/authSlice.js";
const rootReducer = combineReducers({
auth: authReducer,
});
const store = configureStore({
reducer: rootReducer,
});
export default store;
api.js
import axios from "axios";
const token = localStorage.getItem("token");
const baseURL = import.meta.env.VITE_API_BASE_URL;
const api = axios.create({
baseURL
});
api.interceptors.request.use(
(config) => {
if (token) {
const { accessToken } = JSON.parse(token);
config.headers.Authorization = `Bearer ${accessToken}`;
}
// Do something before request is sent
return config;
},
(error) => {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
api.interceptors.response.use(
(response) => {
// Do something with response data
return response;
},
(error) => {
// Do something with response error
return Promise.reject(error);
}
);
export default api;
I try to navigate to "/dashboard"
without refresh but not work and renders a blank page.
The main issue is that the router is re-declared each time the Routes
component is rerendered, e.g. like when the accessToken
state is updated. This interrupts the React rendering cycle and creates a mismatch between the URL which updated correctly and the rendered content.
There were also two routes declared for the same "/"
path. There should only ever be one route per path to be matched.
An additional issue is that your restricted routes are conditionally rendered, so there exists the possibility they are not mounted when you expect them to be.
accessToken
when the route is being accessed, not prior than that to render the route.Example:
src/routes/index.jsx
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectToken } from "../store/auth/authSlice.js";
const PrivateRoutes = () => {
const { accessToken } = useSelector(selectToken);
return accessToken
? <Outlet />
: <Navigate to="/login" replace />;
};
const AnonymousRoutes = () => {
const { accessToken } = useSelector(selectToken);
return accessToken
? <Navigate to="/dashboard" replace />
: <Outlet />;
};
// Define public routes accessible to all users
const routesForPublic = [
{
path: "/service",
element: <div>Service Page</div>,
},
{
path: "/about-us",
element: <div>About Us</div>,
},
];
// Define routes accessible only to authenticated users
const routesForAuthenticatedOnly = [
{
element: <DashboardLayout />,
children: [
{
path: "dashboard",
element: <h1>Dashboard</h1>,
},
],
},
];
// Define routes accessible only to non-authenticated users
const routesForNotAuthenticatedOnly = [
{
path: "/",
element: <div>Home Page</div>,
},
{
path: "/login",
element: <LoginPage />,
},
];
// Combine and conditionally include routes based on authentication status
const router = createBrowserRouter([
// Anyone can access these routes
...routesForPublic,
// Only authenticated users can access these routes
{
element: <PrivateRoutes />,
children: routesForAuthenticatedOnly
},
// Only unauthenticated users can access these routes
{
element: <AnonymousRoutes />,
children: routesForNotAuthenticatedOnly
},
]);
const Routes = () => {
return <RouterProvider router={router}/>;
};
export default Routes;
Additionally, the Login
component should unwrap the resolved result of the asynchronous login
thunk. This is because all createAsyncThunk
actions resolve, but you need to unwrap them to know if they were successful or if there was an error. See Handling Thunk Results for more details.
const onFinish = (values) => {
dispatch(login(values)).unwrap()
.then(() => {
navigate('/dashboard');
})
.catch((error) => {
console.log('Login failed:', error);
});
};
or
const onFinish = async (values) => {
try {
await dispatch(login(values)).unwrap();
navigate('/dashboard');
} catch(error) {
console.log('Login failed:', error);
}
};