I am trying to log a user in. I have a login component which takes the credentials and then I am using redux toolkit to store the state and the verification and everything is done in userSlice. I have a protected route which should check if the user is logged in or not and if the user is not logged in then it should not navigate to the recipe page which I have. when I try to access the user from the protected route component using useSelecter hook it returns empty on the first render but on the second render it does return user but the login still fails. in the redux dev tools the state is being updated just fine. is there a way where I can get the user object on the first render from the protected route component. (As you can see I am using the useEffect hook and have the dependency array too).
Any help would be greatly appreciated. Thank you.
Below are my code:
Login.js -- this file is responsible for taking in the credentials and then dispatching an action and updating the state using useDispatch.
import React, { useState } from 'react';
import { loginUser } from '../../features/users/userSlice';
import { useSelector, useDispatch } from 'react-redux'
import { useNavigate } from 'react-router-dom';
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const user = useSelector(state => state.user.user)
const dispatch = useDispatch()
const navigate = useNavigate()
return (
<div>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" onClick={() => {
dispatch(loginUser({email, password}))
navigate('/recipes')
}}>submit</button>
</div>
)
}
ProtectedRoute.js -- This component makes sure if the user is not authenticated he must not be able to login
import React, { useState, useEffect } from "react";
import { Route, Navigate, Outlet } from "react-router-dom";
import { useSelector } from 'react-redux';
export default function ProtectedRoute({ children }) {
const [ activeUser, setActiveUser ] = useState(false)
const user = useSelector((state) => state.user);
useEffect(() => {
if (!user.isLoading) {
user.success ? setActiveUser(true) : setActiveUser(false)
console.log('active user: ' + activeUser)
}
}, [user])
return (
activeUser ? <Outlet /> : <Navigate to="/login"/>
)
}
app.js -- This component has all the routes including the protected route.
import React from "react";
import Recipes from "./components/recipes/recipes";
import Login from "./components/users/Login";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import ProtectedRoute from "./utils.js/ProtectedRoute";
const App = () => {
return (
<div>
<BrowserRouter>
<Routes>
<Route path="/login" index element={<Login />} />
<Route path="/" element={<Navigate replace to="/login" />}/>
<Route element={<ProtectedRoute />}>
<Route element={<Recipes/>} path="/recipes" />
</Route>
</Routes>
</BrowserRouter>
</div>
);
};
export default App;
userSlice.js -- since I am using redux toolkit hence I have slices for different things. This has reducers which have user state.
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
const loginUrl = 'http://localhost:5000/api/login';
const signupUrl = 'http://localhost:5000/api/signup';
export const loginUser = createAsyncThunk('user/loginUser', async (data) => {
const response = await axios.post(loginUrl, data);
return response;
})
export const signupUser = createAsyncThunk('user/signupUser', async (data) => {
const response = await axios.post(signupUrl, data);
return response;
})
const initialState = {
user: {},
isLoading: true
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
getPassword: (state, action) => {
const password = action.payload
console.log(password)
}
},
extraReducers: {
[loginUser.pending]: (state) => {
state.isLoading = false
},
[loginUser.fulfilled]: (state, action) => {
state.isLoading = false
state.user = action.payload.data
},
[loginUser.rejected]: (state) => {
state.isLoading = false
},
[signupUser.pending]: (state) => {
state.isLoading = false
},
[signupUser.fulfilled]: (state, action) => {
state.isLoading = false
state.user = action.payload.data
},
[signupUser.rejected]: (state) => {
state.isLoading = false
},
}
})
export const { getPassword } = userSlice.actions
export default userSlice.reducer;
The issue is that the login handler is issuing both actions "simultaneously".
onClick={() => {
dispatch(loginUser({ email, password })); // <-- log user in
navigate('/recipes'); // <-- immediately navigate
}}
The navigation action to the protected route occurs prior to the user being authenticated the redux state being updated.
To resolve the login handler should wait for successful authentication then redirect to the desired route.
const loginHandler = async () => {
try {
const response = await dispatch(loginUser({ email, password })).unwrap();
if (/* check response condition */) {
navigate("/recipes", { replace: true });
}
} catch (error) {
// handle any errors or rejected Promises
}
};
...
onClick={loginHandler}
See Handling Thunk Results for more details.
You may also want to simplify the ProtectedRoute
logic to not require an extra rerender just to get the correct output to render. All the route protection state can be derived from selected redux state.
export default function ProtectedRoute() {
const user = useSelector((state) => state.user);
if (user.isLoading) {
return null; // or loading indicator/spinner/etc
}
return user.success ? <Outlet /> : <Navigate to="/login" replace />;
}