I have useAPIRequest
custom hook as follows:
import { useNavigate } from "react-router-dom";
import { routes } from "src/routes/routes";
import {useState} from "react";
import {useLocalStorage} from "src/hooks/useLocaleStorage";
import {useTranslation} from "react-i18next";
interface requestParameters {
url: string;
// eslint-disable-next-line
onSuccess?: (responseData: any) => void;
// eslint-disable-next-line
onError?: (responseData: any) => void;
}
// eslint-disable-next-line
interface getRequestParameters extends requestParameters {}
interface postRequestParameters extends requestParameters{
// eslint-disable-next-line
body?: any;
}
interface fetchRequestParameters extends requestParameters {
method: string;
//eslint-disable-next-line
body?: any;
}
export const useAPIRequest = () => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [success, setSuccess] = useState<boolean | undefined>(undefined);
// eslint-disable-next-line
const [data, setData] = useState<any>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const navigate = useNavigate();
const {removeFromStorage} = useLocalStorage();
const {t} = useTranslation();
const getErrorByID = (errorID: string) => {
switch (errorID){
case "INVALID_CREDENTIALS":
return t("There is no user found with the given credentials")
default:
return t('The data could not be fetched.')
}
}
const fetchRequest = async (parameters: fetchRequestParameters) => {
const {url, method, body = undefined, onSuccess = undefined, onError = undefined} = parameters;
setIsLoading(true);
const requestOptions: RequestInit = {
method: method,
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
};
if (body !== undefined) {
requestOptions.body = JSON.stringify(body);
}
try {
const response = await fetch(url, requestOptions);
const responseData = await response.json();
if (response?.status === 403) {
// Not authenticated - clear localstorage and navigate to login page
removeFromStorage('user');
navigate(routes.LOGIN);
// Show notification
}
setSuccess(response.ok);
setData(response.ok ? responseData : undefined)
setError(response.ok ? undefined : getErrorByID(responseData.id));
setIsLoading(false)
// eslint-disable-next-line no-debugger
debugger;
if(response.ok && onSuccess) onSuccess(responseData)
if(!response.ok && onError) onError(responseData)
// eslint-disable-next-line
} catch (e: any) {
setSuccess(false);
setData(undefined)
setError(t("An error occurred. Fetch request not executed."));
setIsLoading(false)
if(onError) onError(e)
}
}
return {
isLoading,
success,
data,
error,
getRequest: (parameters: getRequestParameters): Promise<void> => {
return fetchRequest({
url: parameters.url,
method: "GET",
onSuccess: parameters?.onSuccess
})
},
postRequest: (parameters: postRequestParameters): Promise<void> => {
return fetchRequest({
url: parameters.url,
method: "POST",
body: parameters?.body,
onSuccess: parameters?.onSuccess
})
}
}
}
Then in a component where I use it:
const { isLoading: submitting, success, data, error, postRequest } = useAPIRequest();
useEffect(() => {
if(success && data){
userDispatch({
type: UserActionKind.LOG_IN,
payload: {
id: data.id,
firstName: data.first_name,
lastName: data.last_name,
},
})
// Navigate to dealers selector page
navigate(routes.DEALER_SELECTOR)
}
}, [success, data]);
Thus, here is everything sync right?
He executes userDispatch which is as follows:
export function UserReducer(
UserState: UserState | never,
action: UserAction
): UserState | never {
switch (action.type) {
case UserActionKind.LOG_IN: {
const newState = {
...UserState,
id: action.payload.id,
isAuthenticated: true,
info: {
...UserState.info,
firstName: action.payload.firstName,
lastName: action.payload.lastName
},
}
localStorage.setItem('user', JSON.stringify(newState))
return newState;
}
default: {
throw Error("Unknown action: " + action.type);
}
}
}
Thus, in the useEffect he should set the state and add it to the localstorage and then he should execute navigate(routes.DEALER_SELECTOR)
But it is not the case, because navigate(routes.DEALER_SELECTOR)
is executed before the data is available in localstorage.
Any idea?
UPDATED
userDispatch is as follows:
export const UserDispatchContext = createContext<
Dispatch<UserAction> | undefined
>(undefined);
export function UserProvider({ children }: { children: React.ReactNode }) {
const userFromStorage = localStorage.getItem('user');
const userState = userFromStorage ? JSON.parse(userFromStorage) : initialUserState;
const [user, dispatch] = useReducer(UserReducer, userState);
console.log('User state: ', user);
return (
<UserContext.Provider value={user}>
<UserDispatchContext.Provider value={dispatch}>
{children}
</UserDispatchContext.Provider>
</UserContext.Provider>
);
}
export function useUserDispatch() {
const context = useContext(UserDispatchContext);
if (context === undefined) {
throw new Error("UserDispatchContext must be within provider");
}
return context;
}
And full Login component is as follows:
import React, {useEffect} from "react";
import backgroundImage from "src/assets/login_background.webp";
import {useUI} from "src/state/uiContext";
import {UserActionKind, useUserDispatch} from "src/state/userContext";
import {API_ENDPOINT} from "src/settings";
import {useLocation, useNavigate} from "react-router-dom";
import {routes} from "src/routes/routes";
import LoginForm from "src/routes/login/components/loginForm";
import {useLocalStorage} from "src/hooks/useLocaleStorage";
import {useAPIRequest} from "src/hooks/useAPIRequest";
import {message} from "antd";
export default function Login() {
const { isLoading: submitting, isFetching, success, data, error, postRequest } = useAPIRequest();
const {getFromStorage} = useLocalStorage();
const isAuthenticated = getFromStorage('user')?.isAuthenticated
const ui = useUI();
const userDispatch = useUserDispatch();
const navigate = useNavigate();
const location = useLocation()
const notice = location?.state?.message;
// Effect to navigate to dealers selector page if user is authenticated
useEffect(() => {
if(isAuthenticated){
navigate(routes.DEALER_SELECTOR)
}
}, [isAuthenticated])
useEffect(() => {
if(success && data && !isFetching){
userDispatch({
type: UserActionKind.LOG_IN,
payload: {
id: data.id,
firstName: data.first_name,
lastName: data.last_name,
},
})
// Navigate to dealers selector page
navigate(routes.DEALER_SELECTOR, {
state: {
isAuthenticated: true
}
})
}
}, [isFetching, success, data]);
// Effect to show a toast notice if user is redirected from the protected page
useEffect(() => {
if(notice){
message.info(notice)
window.history.replaceState({}, document.title)
}
}, []);
const handleSubmit = async (email: string, password: string) => {
// handle the form submission
await postRequest({
url: `${API_ENDPOINT}/auth/log-in`,
body: {
username: email,
password: password
}
})
}
return (
<div
className="align-center flex h-screen w-screen justify-center"
style={backgroundStyle}
>
<div className="flex h-screen w-1/3 flex-col items-center justify-center bg-slate-50/75 p-4">
<img src={ui.assets.logo} className="mb-12 h-fit w-1/2" alt="Logo"/>
<LoginForm submitting={submitting}
error={error}
handleSubmit={handleSubmit}/>
</div>
</div>
);
}
const backgroundStyle = {
background: `url(${backgroundImage})`,
backgroundPosition: "center",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
width: "100vw",
height: "100vh",
};