Why redux state is not render correctly? - javascript

Good day, I faced an issue when tried to push the user to the dashboard after the user login correctly but it didn't, here is the code below:
LoginForm.js
const { isLoading, isAuth, error, message } = useSelector(
(state) => state.login
);
const handleSubmit = (e) => {
e.preventDefault();
console.log(values);//values={email:'..', pass:'..'}
if (formValidation()) {
dispatch(NewUserLogin(values));
console.log(isAuth); //print false but in redux state print true
if (isAuth) history.push('/dashboard');
}
};
LoginAction.js
export const NewUserLogin = (formValues) => async (dispatch) => {
try {
dispatch(loginPending());
const { status, message } = await LoginAPIRequest(formValues);
if (status === 'success') {
dispatch(loginSuccess(message));
} else {
dispatch(loginFailure(message));
}
console.log(status);
console.log(message);
} catch (error) {
dispatch(loginFailure(error.message));
}
};
loginSlice.js
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
isLoading: false,
isAuth: false,
error: '',
};
const loginSlice = createSlice({
name: 'Login',
initialState,
reducers: {
loginPending: (state) => {
state.isLoading = true;
},
loginSuccess: (state, { payload }) => {
state.isLoading = false;
state.isAuth = true;
state.message = payload;
state.error = '';
},
loginFailure: (state, { payload }) => {
//actions.payload or shortcut {payload}
state.isLoading = false;
state.error = payload;
},
},
});
const { reducer, actions } = loginSlice;
export const { loginPending, loginSuccess, loginFailure } = actions;
export default reducer;
userAPI.js
import { createEndpointsAPI, ENDPOINTS } from './index';
export const LoginAPIRequest = (formValues) => {
return new Promise(async (resolve, reject) => {
//call api
try {
await createEndpointsAPI(ENDPOINTS.LOGIN)
.create(formValues)
.then((res) => {
resolve(res.data);
if (res.data.status === 'success') {
resolve(res.data);
sessionStorage.setItem('accessJWT', res.data.accessJWT);
localStorage.setItem('sms', JSON.stringify(res.data.refreshJWT));
}
console.log(res.data);
})
.catch((err) => {
reject(err);
});
} catch (error) {
console.log(error);
reject(error);
}
});
};
index.js (root API)
import axios from 'axios';
export const ENDPOINTS = {
LOGIN: 'user/login',
LOGOUT: 'user/logout',
REGISTER: 'user/register',
};
const baseURL = 'http://localhost:3040/v2/';
export const createEndpointsAPI = (endpoint) => {
let url = baseURL + endpoint + '/';
return {
fetchAll: () => axios.get(url),
fetchById: (id) => axios.get(url + id),
create: (newData) => axios.post(url, newData),
update: (updateData, id) => axios.put(url + id, updateData),
delete: (id) => axios.delete(url + id),
};
};
App.js
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Switch>
<Route path='/' exact>
<Login />
</Route>
<PrivateRoute path='/dashboard'>
<Dashboard />
</PrivateRoute>
<Route path='*' component={() => '404 NOT FOUND'} />
</Switch>
</Router>
</MuiThemeProvider>
PrivateRoute.js
import { useSelector } from 'react-redux';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { isAuth } = useSelector((state) => state.login);
console.log(isAuth);
return (
<Route
{...rest}
render={(props) => {
isAuth ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/',
state: { from: props.location },
}}
/>
);
}}
/>
);
};
export default PrivateRoute;
The problem is, isAuth is a redux state, it should return true when the user login correctly, but it's not, I console.log(isAuth) and it prints false for the first time even user login correctly, and if I click login one more time it prints true in the console log and redirects the user to the dashboard page. I don't know why isAuth is returned false for the first time when use is login correctly? Please help check the above code from top to bottom, I provide you everythings.

The log: console.log(isAuth); logs a stale closure, you could try an effect on isAuth and redirect when it's true.
Here is an example:
const Component = (propps) => {
const { isLoading, isAuth, error, message } = useSelector(
(state) => state.login
);
const handleSubmit = (e) => {
//...dispatches but doesn't check isAuth
};
useEffect(() => {
//go to dashboard if isAuth is true
if (isAuth) history.push('/dashboard');
}, [isAuth]);//run effect when isAuth changes
};

Related

ReactJS: Unable to retrieve properly localStorageItem after navigate to another component

I have this scenario that is after the user login and assuming it is success, user details / user token is stored to localStorage and will automatically navigate to dashboard page, dashboard page has some api calls and those api calls required/needs token that is stored in the localStorage, my problem is that it is unable to retrieve those values in localStorage, but when I check from localStorage using console, the key/value is there, I noticed that, I need to refresh the page to retrieve those details without a problem. How can I possibly fix this issue? to be able to get localStorage value after navigating to another component?
Here is my code for index.tsx
ReactDOM.render(
<AuthContextProvider>
<App />
</AuthContextProvider>,
document.getElementById("root")
);
AuthContext code:
const AuthContext = React.createContext({
user: "",
isLoggedIn: false,
login: (userdata: any, expirationTime: string) => {},
logout: () => {},
});
export const AuthContextProvider = (props: any) => {
const initialUser = localStorage.getItem("user") || "";
const [userData, setUserData] = useState(initialUser);
const userIsLoggedIn = !!userData;
const logoutHandler = () => {
setUserData("");
localStorage.removeItem("user");
};
const loginHandler = async (
user: any,
expirationTime: string
) => {
localStorage.setItem("user", JSON.stringify(user));
setUserData(user);
};
const contextValue = {
user: userData,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
App.tsx code
function App() {
const authCtx = useContext(AuthContext);
return (
<BrowserRouter>
<Routes>
{!authCtx.isLoggedIn && (
<Route element={<LoginLayout />}>
<Route index element={<SignInForm />} />
{/*Other links here */}
</Route>
)}
{authCtx.isLoggedIn && (
<Route element={<AdminLayout />}>
<Route path="dashboard" element={<DashboardScreen />} />
{/*Other links here */}
</Route>
)}
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
);
}
Login code:
try {
await AuthService.login(email, password).then(
(res) => {
authCtx.login(res, "0");
navigate("../dashboard", { replace: true });
},
(error) => {
}
);
} catch (err) {
console.log(err);
}
Dashboard code:
const loadCountOnlineUsers = useCallback(async () => {
try {
await DashboardService.loadCountOnlineUsers().then(
(res) => {
setCntOnlineUsers(res.count);
setflagOnlineUsers(false);
},
(error) => {
setflagOnlineUsers(false);
}
);
} catch (err) {
console.log(err);
setflagOnlineUsers(false);
}
}, [setCntOnlineUsers, setflagOnlineUsers]);
useEffect(() => {
loadCountOnlineUsers();
}, [loadCountOnlineUsers]);
Dashboard service code:
const config = {
headers: {
"Content-Type": "application/json",
Authorization: AuthHeader(),
},
params: {},
};
const loadCountOnlineUsers = () => {
config["params"] = {};
return axios
.get(API_URL + "api/v1/dashboard-related/online-users", config)
.then((response) => {
return response.data;
});
};
const DashboardService = {
loadCountOnlineUsers,
};
export default DashboardService;
Auth-header code:
export default function AuthHeader() {
const user = JSON.parse(localStorage.getItem("user") || "{}");
if (user && user.token) {
return "Bearer " + user.token;
} else {
return "";
}
}
The problem is that the check to localStorage in AuthHeader() isn't updating reactively. The fix would be to rewrite AuthHeader to accept the user data like this:
export default function AuthHeader(user) {
const user = JSON.parse(user || "{}");
if (user && user.token) {
return "Bearer " + user.token;
} else {
return "";
}
}
and then continue the data piping into the area where AuthHeader() is called, perhaps like this:
const config = (user) => ({
headers: {
"Content-Type": "application/json",
Authorization: AuthHeader(),
},
params: {},
});
const loadCountOnlineUsers = (user) => {
config["params"] = {};
return axios
.get(API_URL + "api/v1/dashboard-related/online-users", config(user))
.then((response) => {
return response.data;
});
};
const DashboardService = {
loadCountOnlineUsers,
};
Lastly, using an effect in the dashboard to update it reactively, while connecting to context:
const authCtx = useContext(AuthContext);
const user = authCtx.user;
const loadCountOnlineUsers = (user) => {
return useCallback(async () => {
try {
await DashboardService.loadCountOnlineUsers(user).then(
(res) => {
setCntOnlineUsers(res.count);
setflagOnlineUsers(false);
},
(error) => {
setflagOnlineUsers(false);
}
);
} catch (err) {
console.log(err);
setflagOnlineUsers(false);
}
}, [setCntOnlineUsers, setflagOnlineUsers]);
}
useEffect(() => {
loadCountOnlineUsers(user);
}, [loadCountOnlineUsers, user]);

Redux : useSelector returns undefined but returns data at 3 rd time

I am getting the data from the api as expected but the problem here is I am getting it in a 3rd attempt which is causing the error in my application when there's no data to show.
I am testing it printing on a console but it's the same error. As Soon As I Refresh My Page The Error Comes Flooding In The Console
Reducer
export const productDetailsReducer = (state = { products: {} }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return {
...state,
loading: true,
};
case PRODUCT_DETAILS_SUCCESS:
return {
loading: false,
product: action.payload,
};
case PRODUCT_DETAILS_FAIL:
return {
...state,
error: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
error: null,
};
default:
return state;
}
};
Component
const ProductDetails = () => {
const dispatch = useDispatch();
const alert = useAlert();
const { id } = useParams();
const { product, loading, error } = useSelector(
(state) => state.productDetails
);
useEffect(() => {
dispatch(getProductDetails(id));
if (error) {
alert.error(error);
dispatch(clearErrors());
}
}, [dispatch, id, alert, error]);
console.log(product);
Action
export const getProductDetails = (id) => async (dispatch) => {
try {
dispatch({ type: PRODUCT_DETAILS_REQUEST });
const { data } = await axios.get(`/api/v1/product/${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data.product,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload: error.response.data.message,
});
}
};
App.js
function App() {
return (
<Router>
<div className="App">
<Header />
<div className="container container-fluid">
<Routes>
<Route path="/" element={<Home />} exact />
<Route path="/product/:id" element={<ProductDetails />} />
</Routes>
</div>
<Footer />
</div>
</Router>
);
}
You haven't set the loading in the initial state I think that's why
export const productDetailsReducer = (state = { products: {} }, action) => {
you are trying to access the loading state before you set the value of the state that's why you are getting the error
you should set the loading and error in the initial state with the default values
const initialState = {products: {}, loading: true, error: null}
and then pass it to the reducer.
export const productDetailsReducer = (state = initialState, action) => {
and change the product into products
hopefully, this will fix your issue.
I have checked your code and I think the problem is with your initial state in
export const productDetailsReducer = (state = { products: {} }, action) =>
try changing state={products:{}} with state={product: {}}

React - How to stay on the same page even if it was refreshed?

I'm using react-router for the link to the different pages. Everything works fine, however, once I'll refresh the page, it'll go to the login page for a moment and it'll go back to the homepage. It was even worse if I'll go to the admin page, refreshing the page will direct the user to the login page, however, the user is still logged in and only displays the login page. I'm also using Firebase Firestore and firebase authentication.
app.js
const App = (props) => {
const { setCurrentUser, currentUser } = props;
const admin = checkUserAdmin(currentUser);
console.log(admin);
useEffect(() => {
const authListener = auth.onAuthStateChanged(async (userAuth) => {
if (userAuth) {
const userRef = await handleUserProfile(userAuth);
userRef.onSnapshot((snapshot) => {
setCurrentUser({
id: snapshot.id,
...snapshot.data(),
});
});
}
setCurrentUser(userAuth);
});
return () => {
authListener();
};
}, []);
return (
<div className="App">
<Switch>
<Route
exact
path="/login"
render={() => (
<MainLayout>
<LoginPage />
</MainLayout>
)}
/>
<Route
exact
path="/profile"
render={() => (
<WithAuth>
<MainLayout>
<ProfilePage />
</MainLayout>
</WithAuth>
)}
/>
<Route
exact
path="/admin"
render={() => (
<WithAdmin>
<AdminHome />
</WithAdmin>
)}
/>
</Switch>
</div>
);
};
const mapStateToProps = ({ user }) => ({
currentUser: user.currentUser,
});
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
withAuth - restricting the users for the pages. If currentUser is a guest user, it directs the user to the login page.
import { useAuth } from "./../custom-hooks";
import { withRouter } from "react-router-dom";
const WithAuth = (props) => useAuth(props) && props.children;
export default withRouter(WithAuth);
useAuth - restricting the users for the pages. If currentUser is a guest user, it directs the user to the login page.
const mapState = ({ user }) => ({
currentUser: user.currentUser,
});
const useAuth = (props) => {
const { currentUser } = useSelector(mapState);
useEffect(() => {
if (!currentUser) {
props.history.push("/login");
}
}, [currentUser]);
return currentUser;
};
export default useAuth;
withAdmin - pages only accessible to the admin
import { useAdmin } from "../../custom-hooks";
const WithAdmin = (props) => useAdmin(props) && props.children;
export default WithAdmin;
useAdmin - pages only accessible to the admin. If user is not an admin, it directs the user to the login page.
const mapState = ({ user }) => ({
currentUser: user.currentUser,
});
const useAdmin = (props) => {
const { currentUser } = useSelector(mapState);
const history = useHistory();
useEffect(() => {
if (!checkUserAdmin(currentUser)) {
history.push("/login");
}
}, [currentUser]);
return currentUser;
};
export default useAdmin;
Below is my index.js
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
Reducers:
userTypes:
const userTypes = {
SET_CURRENT_USER: "SET_CURRENT_USER",
};
export default userTypes;
userActions:
import userTypes from "./user.types";
export const setCurrentUser = (user) => ({
type: userTypes.SET_CURRENT_USER,
payload: user,
});
userReducer:
import userTypes from "./user.types";
const INITIAL_STATE = {
currentUser: null,
};
const userReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case userTypes.SET_CURRENT_USER:
return {
...state,
currentUser: action.payload,
};
default:
return state;
}
};
export default userReducer;
rootReducer:
import { combineReducers } from "redux";
import userReducer from "./user/user.reducer";
export default combineReducers({
user: userReducer,
});
store.js
import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
import rootReducer from "./rootReducer";
export const middlewares = [logger];
export const store = createStore(rootReducer, applyMiddleware(...middlewares));
export default store;
checkUserAdmin.js
export const checkUserAdmin = (currentUser) => {
if (!currentUser || !Array.isArray(currentUser.roles)) return false;
const { roles } = currentUser;
if (roles.includes("admin")) return true;
return false;
};
From the App.js, I console.log(currentUser) and this is what is shows:
I suggest adding an authPending state to your userReducer, initially true, and also set/cleared when the firestore logic is handing user changes.
userReducer & actions
const userTypes = {
SET_AUTH_PENDING: "SET_AUTH_PENDING",
SET_CURRENT_USER: "SET_CURRENT_USER",
};
const setAuthPending = pending => ({
type: userTypes.SET_AUTH_PENDING,
payload: pending,
});
const INITIAL_STATE = {
authPending: true,
currentUser: null,
};
const userReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case userTypes.SET_CURRENT_USER:
return {
...state,
authPending: false
currentUser: action.payload,
};
case userTypes.SET_AUTH_PENDING:
return {
...state,
authPending: action.payload,
};
default:
return state;
}
};
app.js
const App = (props) => {
const {
setAuthPending, // <-- access action
setCurrentUser,
currentUser
} = props;
const admin = checkUserAdmin(currentUser);
console.log(admin);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (userAuth) => {
setAuthPending(true); // <-- start auth pending
if (userAuth) {
const userRef = await handleUserProfile(userAuth);
userRef.onSnapshot((snapshot) => {
setCurrentUser({ // <-- will clear auth pending
id: snapshot.id,
...snapshot.data(),
});
});
} else {
setCurrentUser(null); // <-- clear user data and pending
}
});
return () => {
unsubscribe();
};
}, []);
return (
<div className="App">
<Switch>
...
</Switch>
</div>
);
};
const mapStateToProps = ({ user }) => ({
currentUser: user.currentUser,
});
const mapDispatchToProps = {
setAuthPending, // <-- wrap action creator in call to dispatch
setCurrentUser,
};
Hooks & Wrappers
For these I suggest abstracting the logic into custom Route components.
const AuthRoute = props => {
const { authPending, currentUser } = useSelector(state => state.user);
if (authPending) {
return "Loading..."; // or maybe a loading spinner
};
return currentUser ? (
<Route {...props} />
) : (
<Redirect to="/login" />
);
};
const AdminRoute = props => {
const { authPending, currentUser } = useSelector(state => state.user);
if (authPending) {
return "Loading..."; // or maybe a loading spinner
};
return checkUserAdmin(currentUser) ? (
<Route {...props} />
) : (
<Redirect to="/login" />
);
};
Then the routes become
<Switch>
<Route
exact
path="/"
render={() => (
<MainLayout>
<Homepage />
</MainLayout>
)}
/>
<Route
exact
path="/login"
render={() => (
<MainLayout>
<LoginPage />
</MainLayout>
)}
/>
<AuthRoute
exact
path="/profile"
render={() => (
<MainLayout>
<ProfilePage />
</MainLayout>
)}
/>
<AdminRoute
exact
path="/admin"
component={AdminHome}
/>
</Switch>
After this, you may want to look into persisting your redux state into localStorage, and repopulating your redux state from localStorage when you are instantiating the store (the preloadedState parameter) object when your app is loading. You can manage yourself or look into something like redux-persist.
When a user login you can store some values about the user in localStorage,like username or a token or just a login ,
localStorage.setItem(IS_LOGIN, true);
After that you can use that in your userReducer, when you initiate state you can directly determine the user is login or not.
const INITIAL_STATE = {
isLogin: localStorage.IS_LOGIN
};
now you can determine a user is login or not before the page load. If you wanna push user to the login page you can use in useEffect
useEffect(() => {
if (!isLogin) {
props.history.push("/login");
}
}, [isLogin]);
return isLogin;
};
when your app first loaded there is no user information on the userReducer, because of that when page load you will be directing to the login page.

State is null, after useEffect hooks

I am trying to set product by dispatching a method in a useEffect. However, state still says null.
index.html
import React, { useEffect, Fragment } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchProductsData } from "../../store/products-actions";
import Promotion from "./components/Promotion";
import Products from "./components/Products";
import ToastUi from "../../shared/ui/ToastUi";
import { Container, Row, Col } from "react-bootstrap";
const Home = () => {
const dispatch = useDispatch();
// const products = useSelector((state) => state.products.products);
const products = useSelector((state) => state.products.productsTest);
const cartQuantity = useSelector((state) => state.cart.quantity);
useEffect(() => {
dispatch(fetchProductsData());
}, [dispatch]);
return (
<Fragment>
<ToastUi
status="Sukses"
title="Notifikasi"
message={`(${cartQuantity}) produk baru berhasil di masukkan keranjang`}
/>
<Container fluid="xl">
<Row>
<Col>
<Promotion />
</Col>
</Row>
<Row className="mt-3" md={3}>
<Products products={products} />
</Row>
</Container>
</Fragment>
);
};
export default Home;
Products still says null after a cycle, apparently it needs second cycle to make that state changed. Not sure how can I make it change in one cycle. Do I need to put the useEffect in the parent ?
EDIT
if I add this, it will work
{products !== null && <Products products={products} />}
// {/* <Products products={products} /> */} //
However, is there a better way or maybe some explanation on why this is happening, Thank you.
EDIT
products-slice.js
import { createSlice } from "#reduxjs/toolkit";
import {
products,
excel_products,
product,
filteredProducts,
productsTest,
} from "../datafiles";
const initialProductsState = {
products,
excel_products,
product,
filteredProducts,
productsTest,
};
const productsSlice = createSlice({
name: "products",
initialState: initialProductsState,
reducers: {
viewExcelProducts(state, action) {
state.excel_products = action.payload;
},
uploadExcelProducts(state) {
if (excel_products.length < 0) {
console.log("error");
} else {
const newProducts = state.products.concat(state.excel_products);
state.products = newProducts;
state.excel_products = [];
}
},
selectProduct(state, action) {
const product = state.products.find((item) => item.id === action.payload);
state.product = product;
},
filterProducts(state, action) {
const filteredProducts = state.products.filter(
(item) => item.type === action.payload
);
state.filteredProducts = filteredProducts;
},
setProducts(state, action) {
state.productsTest = action.payload;
},
},
});
export const productsActions = productsSlice.actions;
export default productsSlice;
products-actions.js
import { productsActions } from "./products-slice";
export const fetchProductsData = () => {
return async (dispatch) => {
const fetchData = async () => {
const response = await fetch("http://localhost:5000/products");
if (!response.ok) {
throw new Error("Could not fetch data!");
}
const data = await response.json();
return data;
};
try {
const productsData = await fetchData();
dispatch(productsActions.setProducts(productsData));
} catch (err) {
console.log("Error: " + err);
}
};
};
What do you mean by it needs second cycle to make that state changed?
fetchProductsData is an async function, I assume. That means that you do not receive data immediately, but after some time (depending on network connection speed, payload size etc). So it is OK that your data arrives later.
Usual approach for async data is to keep isLoading in your state. And use it as following:
const isLoading = useSelector((state) => state.products.isLoading);
...
return (
<Fragment>
...
{isLoading && <Spinner />} // Some loading indicator
{!isLoading && <Products products={products} />}
</Fragment>
);
This way you will indicate to user that some data is being fetched. This is a good UX approach.
isLoading should be set somewhere in your fetchProductsData action, like so:
export const fetchProductsData = () => {
return async (dispatch) => {
...
try {
dispatch(productsActions.setIsLoading(true));
const productsData = await fetchData();
dispatch(productsActions.setProducts(productsData));
} catch (err) {
console.log("Error: " + err);
} finally {
dispatch(productsActions.setIsLoading(false));
}
};
};

How do you wait for useAuth useEffect to return the current user state?

I have a bit of a problem implementing authentication for my React application. I followed this link to get the authentication going. Here's my App component:
function App() {
return (
<ProvideAuth>
<BrowserRouter>
<Header />
<Switch>
<PrivateRoute exact path="/">
<Dashboard />
</PrivateRoute>
<Route path="/login">
<Login />
</Route>
</Switch>
</BrowserRouter>
</ProvideAuth>
);
}
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
console.log("USER: ", auth.user);
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)} />
)
}
export default App;
Login component:
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
let history = useHistory();
let location = useLocation();
let auth = useAuth();
let { from } = location.state || { from: { pathname: "/" } }
let login = (e) => {
auth.signin(email, password, () => {
history.replace(from);
});
};
return (
<div>
<input onChange={e => setEmail(e.target.value)} value={email} type="email" />
<input onChange={e => setPassword(e.target.value)} value={password} type="password" />
</div>
)
}
export default Login;
Finally use-auth.js:
const authContext = createContext();
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};
export const useAuth = () => {
return useContext(authContext);
};
function useProvideAuth() {
const [user, setUser] = useState(null);
const signin = (email, password, callback) => {
axios.post(`${apiUrl}/sign_in`, {
'email': email,
'password': password
},
{
headers: {
'Content-Type': 'application/json'
}
}).then(res => {
const expiryDate = new Date(new Date().getTime() + 6 * 60 * 60 * 1000).toUTCString();
document.cookie = `access-token=${res.headers['access-token']}; path=/; expires=${expiryDate}; secure; samesite=lax`;
return res.data
})
.then(data => {
setUser(data.data);
callback();
})
.catch(e => {
setUser(null);
});
};
const signout = () => {
document.cookie = "access-token=; expires = Thu, 01 Jan 1970 00:00:00 GMT";
setUser(null);
}
useEffect(() => {
const cookies = getCookies();
if (cookies['access-token']) {
axios.get(`${apiUrl}/user_info`, {
headers: {
...cookies
}
}).then(res => {
return res.data;
})
.then(data => {
setUser(data);
})
.catch(e => {
setUser(null);
})
} else {
setUser(null);
}
}, []);
return {
user,
signin,
signout
}
}
function getCookies() {
let cookies = document.cookie.split(';');
let authTokens = {
'access-token': null
};
for (const cookie of cookies) {
let cookiePair = cookie.split('=');
if (authTokens.hasOwnProperty(cookiePair[0].trim().toLowerCase()))
authTokens[cookiePair[0].trim()] = decodeURIComponent(cookiePair[1]);
}
return authTokens;
}
and then the dashboard component is the homepage. Nothing interesting.
The problem is when a user is in fact logged in (the access-token cookie is set as well as other tokens), they're still routed to the login page because of the fact that calling the API which checks that these tokens are valid is asynchronous, so the user is set to null initially.
What am I missing here? how can I wait until the API response is returned without blocking the user interface? Should I save user state in the redux state or is there some other work around?
Thanks a lot!
Like Jonas Wilms suggested, I added a loading state variable in user-auth similar to user and set it to true before each request and false after the request is completed.
In my App component, I changed the PrivateRoute function to show a loading spinner as long as the user state is loading. When it's set to false, I check whether the user is logged in or not and show the Dashboard component or redirect to login page accordingly.
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
return (
<Route
{...rest}
render={({ location }) =>
auth.loading ?
<Loading /> :
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)} />
)
}

Categories

Resources