0

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",
};
Boky
  • 11,554
  • 28
  • 93
  • 163
  • Can you show the hole component where you have that `useEffect` that contains that `userDispatch` call? I think you want to say is why it's navigating before the store gets updated, because the local storage normally would be so far. – Youssouf Oumar Feb 23 '23 at 16:02
  • What is `userDispatch` and where is `UserReducer` being called? Please [edit] the post to include a more complete [mcve] so it's clear to readers what the app is doing step-by-step. I suspect that `userDispatch` is updating some React state, which is an asynchronous process, i.e. not immediately processed. We can't know for sure until we see all the relevant code. – Drew Reese Feb 23 '23 at 16:37
  • Ok, so the `useReducer` "state" behaves much in the same way as the regular old `useState` state with regards to ***when*** it is updated. React state updates are processed asynchronously *between* render cycles. Basically what this means is that `userDispatch(....)` ***enqueues*** a state update ***synchronously*** and immediately after that `navigate(....)` is called and the navigation action is effected. You might just need to move the navigation logic into a `useEffect` hook that can "react" to so state being updated. Does this make sense? – Drew Reese Feb 24 '23 at 06:51

0 Answers0