Search code examples
reactjsspringredux

Redux hooks problem while sending request to the backend


First of all I am newbie at redux. I'm trying to create e-commerce website. At the back end side I'm handling login in the below code with spring.

@PostMapping("/login")
    public ResponseEntity<AuthResponse> authenticate(
            @RequestBody LoginRequest request
    ){
        return ResponseEntity.ok(authService.authenticate(request));
    }
public AuthResponse authenticate(LoginRequest request) {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getEmail(),
                        request.getPassword()
                )
        );
        var user = userRepository.findByEmail(request.getEmail())
                .orElseThrow();
        var jwtToken = jwtService.generateToken(user);
        return AuthResponse.builder()
                .token(jwtToken)
                .build();
    }

When a user logs in, I can access their email through JWT. However, my challenge begins here. Upon logging in, I only have the user's email in React, and I need to retrieve the user's data using this email. In React, I am attempting to send the logged user's email to the backend to access their data, but I am having an error related to frontend mistakes.

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)

My react-redux codes which is related with the error,

const handleSubmit = async () => {
        try {
            const userData = await login({ email: formikSignIn.values.signEmail , password: formikSignIn.values.signPassword }).unwrap();
            const data = await useGetLoggedUserQuery(formikSignIn.values.signEmail);
            console.log(data);
            dispatch(setCredentials({ ...userData, email: formikSignIn.values.signEmail }));
            setIsModalLoggedInOpen(true);
            setTimeout(() => {
                navigate('/home');
            }, 2000);
        } catch (err: any) {
            console.log(err.status);
            if (err.status === "FETCH_ERROR") {
                showErrorModal("No server response!", "Server is under maintenance, please try again later.");
            } else {
                if (err.status === 403) {
                    showErrorModal("Wrong email or password", "Please check your login informations again.");
                } else {
                    showErrorModal("Login failed!", "Please try again.");
                    console.log(err);
                }
            }
        }
    }
export type UserState = {
    id: number;
    firstName: string;
    lastName: string;
    email: string;
    address?: string;
    role: Role;
  };

export type AuthState = {
    isAuthenticated: boolean;
    user?: UserState; 
    token?: string;
};
  

enum Role {
    ADMIN = "ADMIN",
    USER = "USER",
}
export const authApiSlice = apiSlice.injectEndpoints({
    endpoints: builder => ({
        login: builder.mutation({
            query: credentials => ({
                url: '/auth/login',
                method: 'POST',
                body: { ...credentials }
            })
        }),
        getLoggedUser: builder.query({
            query: userEmail => `/user/${userEmail}`,  
        }),
    })
})

export const {
    useLoginMutation,
    useGetLoggedUserQuery 
} = authApiSlice
export const store = configureStore({
    reducer: {
        [apiSlice.reducerPath]: apiSlice.reducer,
        auth: authReducer
    },
    middleware: getDefaultMiddleware =>
        getDefaultMiddleware().concat(apiSlice.middleware),
    devTools: true
})
const baseQuery = fetchBaseQuery({
    baseUrl: 'http://localhost:8080',
    credentials: 'include',
    prepareHeaders: (headers, { getState }) => {
        const state = getState() as AuthState;
        const token = state.token;
        if (token) {
            headers.set("authorization", `Bearer ${token}`);
        }
        return headers;
    }
})

const baseQueryWithReauth = async (args: any, api: any, extraOptions: any) => {
    let result = await baseQuery(args, api, extraOptions);

    if (result?.error && 'originalStatus' in result.error){
        if (result?.error?.originalStatus === 403) {
            console.log('sending refresh token');
            const refreshResult = await baseQuery('/refresh', api, extraOptions);
            console.log(refreshResult);
            if (refreshResult?.data) {
                const user = api.getState().user;
                api.dispatch(setCredentials({ ...refreshResult.data, user }));
                result = await baseQuery(args, api, extraOptions);
            } else {
                api.dispatch(logOut({}));
            }
        }
    }
    return result;
}

export const apiSlice = createApi({
    baseQuery: baseQueryWithReauth,
    endpoints: builder => ({})
})

Also, shouldn't using React with Redux result in less code in React components? Where am I making a mistake by writing too much codes in the functions for backend communication?


Solution

  • Calling the useGetLoggedUserQuery hook in the handleSubmit callback breaks React's Rules of Hooks.

    Only Call Hooks at the Top Level

    Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

    I suggest you export and use the Lazy Query Hook that returns a trigger function that can be used in your callback.

    Example:

    export const authApiSlice = apiSlice.injectEndpoints({
      endpoints: builder => ({
        ....
        getLoggedUser: builder.query({
          query: userEmail => `/user/${userEmail}`,  
        }),
      })
    })
    
    export const {
      useLoginMutation,
      useGetLoggedUserQuery,
      useLazyGetLoggedUserQuery, // <-- lazy query hook
    } = authApiSlice
    
    import { useLazyGetLoggedUserQuery } from '../path/to/apiSlice';
    
    ...
    
    const [getLoggedUser] = useLazyGetLoggedUserQuery();
    
    ...
    
    const handleSubmit = async () => {
      try {
        const userData = await login({
          email: formikSignIn.values.signEmail,
          password: formikSignIn.values.signPassword
        }).unwrap();
    
        const data = await getLoggedUser(formikSignIn.values.signEmail).unwrap();
    
        console.log(data);
        dispatch(setCredentials({
          ...userData,
          email: formikSignIn.values.signEmail
        }));
        setIsModalLoggedInOpen(true);
        setTimeout(() => {
          navigate('/home');
        }, 2000);
      } catch (err: any) {
        console.log(err.status);
        if (err.status === "FETCH_ERROR") {
          showErrorModal(
            "No server response!",
            "Server is under maintenance, please try again later."
          );
        } else {
          if (err.status === 403) {
            showErrorModal(
              "Wrong email or password",
              "Please check your login informations again."
            );
          } else {
            showErrorModal("Login failed!", "Please try again.");
            console.log(err);
          }
        }
      }
    };