I have a React application with a home or landing page and a dashboard, I want the dashboard components to persist after the page reloads, but whenever I refresh, it navigates back to my home page
AuthSlice
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import authService from './authService';
import { jwtDecode } from 'jwt-decode';
const initialState = {
isAuthenticated: false,
data:{
user: {}
} ,
loading: false,
isError: false,
isSuccess: false,
isLoading: false,
message: '',
getcpd: null,
employeeCode: { portalCode: '' },
errors: null,
};
console.log('initial-redux state : ',initialState.user)
// Async thunk to initialize authentication state during application startup
export const initializeAuthState = createAsyncThunk(
'auth/initializeAuthState',
async () => {
console.log('checking local storage...');
const storedToken = localStorage.getItem('jwtToken');
if (storedToken) {
console.log('token present in local storage!');
const decoded = jwtDecode(storedToken);
try {
const response = await authService.retrieveUserData(decoded.id);
console.log('userData retrieved during initialization', response);
// Return the payload to be used in extraReducers
return {
decoded,
userData: response.data,
};
} catch (error) {
console.error('Error during authentication state initialization:', error);
throw error; // Propagate the error
}
} else {
console.log('No token in local storage!');
return null; // Return null if no token is present
}
}
);
export const register = createAsyncThunk(
'auth/register',
async (userData, { rejectWithValue }) => {
try {
await authService.register(userData);
return { success: true };
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Login user
export const login = createAsyncThunk(
'auth/login',
async (user, thunkAPI) => {
try {
const response = await authService.login(user);
console.log('login response from service', response);
if (response.token) {
// Use jwtDecode directly
const decoded = jwtDecode(response.token);
console.log('decoded:', decoded);
localStorage.setItem('jwtToken', response.token);
authService.setAuthToken(response.token);
// Dispatch the setCurrentUser action using thunkAPI
thunkAPI.dispatch(setCurrentUser({
...decoded, // Include the decoded token properties
...response.data, // Include additional user information from response.data
}));
// Return the response data
return response;
} else {
console.log('Token is not available in the response data.');
// Handle the absence of token, you may want to throw an error or handle it as needed
throw new Error('Token is not available in the response data.');
}
} catch (error) {
// Handle the error and return a rejected value using rejectWithValue
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setCurrentUser: (state, action) => {
console.log('setcurrentUser payload:', action.payload)
state.isAuthenticated = !!Object.keys(action.payload).length;
state.user = action.payload;
},
setUserLoading: (state) => {
state.loading = true;
},
resetErrors: (state) => {
state.errors = null;
},
},
extraReducers: (builder) => {
builder
.addCase(initializeAuthState.fulfilled, (state, action) => {
state.isAuthenticated = true;
state.data.user = {
...action.payload.decoded,
...action.payload.userData,
};
})
.addCase(initializeAuthState.rejected, (state, action) => {
console.error('Error during authentication state initialization:', action.error);
})
.addCase(login.fulfilled, (state) => {
state.loading = false;
})
.addCase(login.rejected, (state, action) => {
state.loading = false;
state.errors = action.payload;
})
},
});
export const { setCurrentUser, setUserLoading, resetErrors } =
authSlice.actions;
export default authSlice.reducer;
Then I am importing initializeAuthState in app.jsx
export default function App() {
const dispatch = useDispatch();
//Call initializeAuthState when the app starts
useEffect(() => {
dispatch(initializeAuthState());
});
useScrollToTop();
return (
<ThemeProvider>
<ErrorBoundary>
<Router />
</ErrorBoundary>
</ThemeProvider>
);
}
My handleSubmit function for login
const handleSubmitLogin = async () => {
// Dispatch the login action
const actionResult = await dispatch(login(userLogin));
// Check if the login action was successful
if (login.fulfilled.match(actionResult)) {
console.log('isAuthenticated:', isAuthenticated); // Add this line
console.log('user:', user); // Add this line
// If already authenticated, stay on the current page
if (isAuthenticated) {
console.log('Already authenticated, not navigating.'); // Add this line
return;
}
// If not authenticated, navigate to the home page
router.push('/home');
}
};
and lastly my Route file handler
export default function Router() {
// const { isSuccess } = useSelector((state) => state.auth);
const storedIsSuccess = localStorage.getItem('isSuccess') === 'true';
const { isAuthenticated } = useSelector((state) => state.auth);
const { user } = useSelector((state) => state.auth );
const location = useLocation();
const isReload = location.state && location.state.isReload;
useEffect(() => {
if (isAuthenticated && isReload) {
// Prevent navigation away from the current page during refresh
return;
}
}, [isAuthenticated, isReload]);
console.log(isAuthenticated && 'user authenticated');
console.log('user before login starts',user);
return (
<Routes>
<Route
path="/"
element={
(user && isAuthenticated) ? (
<DashboardLayout>
<DashboardLayout>
<Suspense>
<Outlet />
</Suspense>
</DashboardLayout>
) : (
<HomePage />
)
}
>
I want user to be able to refresh a page and it doesn't return to home page.
There are no navigation actions occurring in the app. The initial isAuthenticated
state is false
so when the page loads and the app mounts, Router
conditionally renders the HomePage
on the only route that exists, "/"
on the initial render cycle.
Start from an undefined
initial isAuthenticated
state value and conditionally null or some loading indicator while the initializeAuthState
action processes and sets the isAuthenticated
state based on the stored JWT token.
const initialState = {
isAuthenticated: undefined, // <-- we don't initially know auth state 🤷🏻♂️
data: {
user: {}
} ,
loading: false,
isError: false,
isSuccess: false,
isLoading: false,
message: '',
getcpd: null,
employeeCode: { portalCode: '' },
errors: null,
};
Update the initializeAuthState
action to catch thrown errors and rejected Promises and return a rejected value from the Thunk.
export const initializeAuthState = createAsyncThunk(
'auth/initializeAuthState',
async (_, thunkApi) => {
try {
const storedToken = localStorage.getItem('jwtToken');
const decoded = jwtDecode(storedToken);
const { data } = await authService.retrieveUserData(decoded.id);
return {
decoded,
userData: data,
};
} catch (error) {
return thunkApi.rejectWithValue(error);
}
}
);
Update the auth slice reducers to handle both token check success/failure.
...
.addCase(initializeAuthState.fulfilled, (state, action) => {
state.isAuthenticated = true;
state.data.user = action.payload;
state.isError = false;
state.errors = null;
})
.addCase(initializeAuthState.rejected, (state, action) => {
state.isAuthenticated = false;
state.data.user = {};
state.isError = true;
state.errors = action.payload;
})
...
I suggest creating home route layout route component to abstract the auth check and conditional rendering of page content.
const Home = () => {
const { isAuthenticated } = useSelector((state) => state.auth);
if (isAuthenticated === undefined) {
return null; // <-- or loading indicator/spinner/etc
}
return isAuthenticated
? (
<DashboardLayout>
<Suspense>
<Outlet />
</Suspense>
</DashboardLayout>
) : <HomePage />;
};
export default function Router() {
....
return (
<Routes>
<Route path="/" element={<Home />}>
....
</Route>
</Routes>
);
}