I'm encountering an issue in my React application where I make an API call to a sign-in endpoint. The response from the API returns correctly, indicating successful authentication. However, I'm facing two problems:
The apiData and isAuth states are not updating in time, causing the navigation to the sign-in page to occur before these states are updated. Consequently, the application navigates to the sign-in page too quickly, without waiting for the state updates.
useApi hook
import axios, { AxiosResponse } from "axios";
import { useEffect, useRef, useState } from "react";
const useApi = <T,>(url: string, method: string, payload?: T) => {
const [apiData, setApiData] = useState<AxiosResponse>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
useEffect(() => {
const getData = async () => {
try {
const response = await axios.request({
params: payload,
method,
url,
});
setApiData(response.data);
} catch (e) {
setError(e.message);
} finally {
setLoading(true);
}
};
getData();
}, []);
return { apiData, loading, error };
};
export default useApi;
this is the form
import { useState } from "react";
import { useNavigate } from "react-router-dom";
export const SignIn = () => {
const [userInfo, setUserInfo] = useState({
user: "",
password: "",
});
const navigate = useNavigate();
const submitForm = (e) => {
e.preventDefault();
navigate("/user/sign-in", { state: userInfo });
};
const updateUser = (newVal: string) => {
setUserInfo((prev) => ({
...prev,
user: newVal,
}));
};
const updatePassword = (newVal: string) => {
setUserInfo((prev) => ({
...prev,
password: newVal,
}));
};
return (
<form onSubmit={(e) => submitForm(e)}>
<label>username</label>
<input type="text" onChange={(e) => updateUser(e.target.value)} />
<label htmlFor="">password</label>
<input type="password" onChange={(e) => updatePassword(e.target.value)} />
<button type="submit">submit</button>
</form>
);
};
privateRoute where I check if user is auth or not
import { useEffect, useState } from "react";
import useApi from "../../api/useApi";
import { Navigate, useLocation } from "react-router-dom";
import UserDashboard from "./UserDashboard";
export const PrivateRoute = () => {
const location = useLocation();
const { apiData, loading, error } = useApi(
import.meta.env.VITE_SIGN_IN,
"GET",
location.state,
);
const [isAuth, setAuth] = useState<boolean>(false);
useEffect(() => {
if (apiData?.status === 200) {
console.log(apiData, "api apiData");
setAuth(true);
}
}, [apiData]);
return (
<>
{isAuth ? (
<UserDashboard />
) : (
<Navigate to="/sign-in" state={{ from: location.pathname }} />
)}
</>
);
};
The initial isAuth
state, false
, matches the confirmed-later unauthenticated value false
, so on the initial render cycle the navigation action to the login page is effected. The useEffect
hook runs at the end of the render cycle.
Use an initial isAuth
state value that is neither true
for authenticated nor false
for unauthenticated, e.g. you need a third value to indicate "unknown" status. Use this third value to conditionally render null or any sort of loading indicator while the authentication check runs and confirms the user's status.
export const PrivateRoute = () => {
const location = useLocation();
const { apiData, loading, error } = useApi(
import.meta.env.VITE_SIGN_IN,
"GET",
location.state,
);
const [isAuth, setAuth] = useState<boolean | undefined>(undefined);
useEffect(() => {
if (apiData) {
console.log(apiData, "api apiData");
setAuth(apiData.status === 200);
}
}, [apiData]);
if (isAuth === undefined) {
return null; // or loading spinner/indicator/etc
}
return isAuth
? <UserDashboard />
: (
<Navigate
to="/sign-in"
state={{ from: location.pathname }}
replace
/>
);
};
The isAuth
value appears to be a derived value, so it doesn't actually need to be a React state at all, and is actually generally considered to be a React anti-pattern doing so. You could just compute isAuth
directly from the apiData
.
export const PrivateRoute = () => {
const location = useLocation();
const { apiData, loading, error } = useApi(
import.meta.env.VITE_SIGN_IN,
"GET",
location.state,
);
// undefined | true | false
const isAuth = apiData && apiData.status === 200;
if (isAuth === undefined) {
return null; // or loading spinner/indicator/etc
}
return isAuth
? <UserDashboard />
: (
<Navigate
to="/sign-in"
state={{ from: location.pathname }}
replace
/>
);
};