skip to Main Content

I’ve been at it for 3 days now and I need some help.

I’m working on a project from a course I follow where the use of React + Redux Toolkit (RTK) is mandatory.

I found RTK Query and I managed to send via POST, a login + password to my API and get the proper response from my Express server (back-end was already done and given to me): Login incorrect, password incorrect, login successful.

I can properly console log my token afterwards.

But then, I need to do another POST to retrieve a profile (firstname, lastname, username) and for this, I need to put the received token in my POST headers. And this is where I’m stuck.

I have no idea how to debug all this. I spent the last two days watching/reading tutorials, documentation, I even asked ChatGPT, to no avail. I can’t fill out my POST headers with the token and since my token is always undefined, the issue must be here:

const token = getState().auth.data.body.token;

I can’t figure out what the path should be to retrieve the token.

I’m pretty sure the answer is easy and that I’m missing something obvious and in front of my eyes, but I don’t find the issue.

Here is my API (localhost:3001/api/v1):

POST user/login

response body: (this one works)

{
  "status": 200,
  "message": "User successfully logged in",
  "body": {
    "token": ""
  }
}

POST user/profile

response body: (this one I can’t retrieve)

{
  "status": 200,
  "message": "Successfully got user profile data",
  "body": {
    "email": "[email protected]",
    "firstName": "firstnamE",
    "lastName": "lastnamE",
    "userName": "Myusername",
    "createdAt": "2023-07-18T01:00:34.077Z",
    "updatedAt": "2023-08-02T01:17:22.977Z",
    "id": "64b5e43291e10972285896bf"
  }
}

I don’t post the user update as it is not relevant here.

Here are my files:

store.js:

import { configureStore } from '@reduxjs/toolkit'
import { bankApi } from './ApiSlice.js'

export const store = configureStore({
  reducer: {
    // Add the generated reducer as a specific top-level slice
    [bankApi.reducerPath]: bankApi.reducer,
  },
  // Adding the api middleware enables caching, invalidation, polling,
  // and other useful features of `rtk-query`.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(bankApi.middleware),
})

ApiSlice.js:

// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

const apiBaseUrl = 'http://localhost:3001/api/v1';

// Define a service using a base URL and expected endpoints
export const bankApi = createApi({
  reducerPath: 'bankApi',
  baseQuery: fetchBaseQuery({
    baseUrl: apiBaseUrl,
    prepareHeaders: (headers, { getState }) => {
      console.log('prepareHeaders is called');
      const token = getState().auth.data.body.token;
      if (token) {
        headers.set('Authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  endpoints: (builder) => ({
    auth: builder.mutation({
      query: (credentials) => ({
        url: '/user/login',
        method: 'POST',
        body: credentials,
      }),
    }),
    getProfile: builder.mutation({
      query: () => ({
        url: '/user/profile',
        method: 'POST',
      }),
    }),
    updateProfile: builder.query({
      query: () => ({
        url: '/user/profile',
        method: 'PUT',
      }),
    }),
  }),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const {
  useAuthMutation,
  useGetProfileMutation,
  useUpdateProfileQuery
} = bankApi

Loginpage.jsx:

import { useState } from 'react';
import { useAuthMutation, useGetProfileMutation } from '../rtk/ApiSlice'

export default function LoginPage(){
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const [
    login,
    {
      isLoading: loginIsLoading,
      isError: loginIsError,
      isSuccess: loginIsSuccess,
      data: loginData,
      error: loginError
    }
  ] = useAuthMutation();

  const [
    profile,
    {
      isError: profileIsError,
      error: profileError,
      data: profileData
    }
  ] = useGetProfileMutation();
    
  const handleLogin = (e) => {
    e.preventDefault();
    login({ email, password });
  };

  const handleProfile = () => {
    profile();
  };
    
  return (
    <div className="main bg-dark">
      <section className="sign-in-content">
        <i className="fa fa-user-circle sign-in-icon"></i>
        <h1>Sign In</h1>
        <form
          onSubmit={(e) => {
            e.preventDefault();
            handleLogin();
          }}
        >
          <div className="input-wrapper">
            <label>Username</label>
            <input
              type="text"
              id="email"
              placeholder=""
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
          </div>
          <div className="input-wrapper">
            <label>Password</label>
            <input
              type="password"
              id="password"
              placeholder=""
              value={password}
              onChange={(e) => setPassword(e.target.value)}
            />
          </div>
          <div className="input-remember">
            <input type="checkbox" id="remember-me" />
            <label>Remember me</label>
          </div>
          <button
            className="sign-in-button"
            type="submit"
            disabled={loginIsLoading}
          >
            {loginIsLoading ? 'Signing in...' : 'Sign in'}
          </button>
        </form>
        {loginIsError && <p className='perror'>
          {loginError.data.status} {loginError.data.message}
        </p>}
        {loginIsSuccess && <>
          <p className='psuccess'>
            {loginData.message} Token: {loginData.body.token}
          </p>
          <button onClick={handleProfile}>
            Get Profile
          </button>
        </>}

        {profileIsError && <p className='perror'>
          {profileError.data.status} {profileError.data.message}
        </p>}
        {profileData && <p className='psuccess'>
          {profileData.message} First Name: {profileData.body.firstName} Last Name: {profileData.body.lastName} Surname: {profileData.body.userName}
        </p>}   
      </section>
    </div>
  );
}

What it looks like:

What I tried:

  • Read the documentation about RTK Query
  • Watch RTK Query tutorials
  • Tried to retrieve the token and set it via prepareHeaders
  • Tried to retrieve the token with prepareHeaders in the getProfile query
  • Tried to pass the token to useGetProfileMutation in Loginpage.jsx
  • Tried to store the token in localStorage and retrieve it to pass it to useGetProfileMutation
  • Tried to wait for the login to be successful to then call useGetProfileMutation

2

Answers


  1. The main issue in your code is you are not persisting in your auth response. That’s why your getState() method is not able to consume token. So, you have to persist your token using localStorage or any persisting library like redux-persist. Here I am gonna show modified code using redux-persist.

    Create a slice named "authSlice.js" like this to store auth response:

    import { createSlice } from "@reduxjs/toolkit";
    
    
    const initialState = {
        isLoggedIn: false,
        userDetails: {}
    };
    
    
    export const authSlice = createSlice({
        name: 'auth',
        initialState,
        reducers: {
            logIn: (state,action) => {
                state.isLoggedIn = true
                state.userDetails = action.payload
            },
            logOut: (state) => state = initialState,
        },
    });
    
    export const { logIn, logOut, isLoggedIn } = authSlice.actions;
    export default authSlice.reducer
    

    then install redux-persist and under

    store.js:

    import { configureStore, combineReducers } from '@reduxjs/toolkit'
    import { bankApi } from './ApiSlice.js'
    import authReducer from './authSlice.js'
    
    import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE, persistStore, 
    persistReducer } from 'redux-persist'
    
    
    const persistConfig = {
       key: 'root',
       version: 1,
       storage: sessionStorage,
       blacklist: [bankApi.reducerPath]
    }
    const rootReducer = combineReducers({ // added combineReducer since we have now two reducer
       auth: authReducer,
       [bankApi.reducerPath]: bankApi.reducer,
    })
    const persistedReducer = persistReducer(persistConfig, rootReducer); // persisted our slice data using redux-persist
    export const store = configureStore({
      reducer: persistedReducer, 
      // Adding the api middleware enables caching, invalidation, polling,
      // and other useful features of `rtk-query`.
      middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
      serializableCheck: { // if you are not using nextj, don't add this object
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
      }).concat(bankApi.middleware),
    })
    
        
    

    apiSlice.js:

    // Need to use the React-specific entry point to import createApi
        import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
        
    const apiBaseUrl = 'http://localhost:3001/api/v1';
    
    // Define a service using a base URL and expected endpoints
    export const bankApi = createApi({
        reducerPath: 'bankApi',
        baseQuery: fetchBaseQuery({
            baseUrl: apiBaseUrl,
            prepareHeaders: (headers, { getState }) => {
            console.log('prepareHeaders is called');
            const token = getState().auth.userDetails.token; // we are now consuming token from new created authSlice
            if (token) {
              headers.set('Authorization', `Bearer ${token}`);
            }
            return headers;
        },
        }),
        endpoints: (builder) => ({
            auth: builder.mutation({
                query: (credentials) => ({
                    url: '/user/login',
                    method: 'POST',
                    body: credentials,
                }),
            }),
            getProfile: builder.mutation({
                query: () => ({
                    url: '/user/profile',
                    method: 'POST',
                }),
            }),
            updateProfile: builder.query({
                query: () => ({
                    url: '/user/profile',
                    method: 'PUT',
                }),
            }),
        }),
    })
    
    // Export hooks for usage in functional components, which are
    // auto-generated based on the defined endpoints
    export const { useAuthMutation, useGetProfileMutation, useUpdateProfileQuery } = bankApi
    

    Now modify login page as below:
    Login.jsx

    import { useState } from 'react';
    import { useAuthMutation, useGetProfileMutation } from '../rtk/ApiSlice'
    import {logIn} from '../rtk/authSlice' // will use to persist auth response
    export default function LoginPage(){
    
        const [email, setEmail] = useState('');
        const [password, setPassword] = useState('');
    
        const [login, { isLoading: loginIsLoading, isError: loginIsError, isSuccess: loginIsSuccess, data: loginData, error: loginError }] = useAuthMutation();
    
        const [profile, { isError: profileIsError, error: profileError, data: profileData }] = useGetProfileMutation();
        
        const handleLogin = (e) => {
            e.preventDefault();
            login({ email, password });
        };
      
    
        const handleProfile = () => {
            profile();
          };
        // here we are storing the auth response(token) while loginSuccess is true. And redux persist will automatically persist this for us
       // While you wanna logout call the logOut method of authSlice it will handle your logout scenario
        useEffect(() => {
           if(loginData && loginIsSuccess){
          logIn({token:loginData.data.token});
         }
        },[loginData, loginIsSuccess]); // assuming loginData contains token
        
        return (
            <div className="main bg-dark">
                <section className="sign-in-content">
                    <i className="fa fa-user-circle sign-in-icon"></i>
                    <h1>Sign In</h1>
                    <form onSubmit={(e) => { e.preventDefault(); handleLogin(); }}>
                        <div className="input-wrapper">
                            <label>Username</label>
                            <input type="text" id="email" placeholder="" value={email} onChange={(e) => setEmail(e.target.value)}/>
                        </div>
                        <div className="input-wrapper">
                            <label>Password</label>
                            <input type="password" id="password" placeholder="" value={password} onChange={(e) => setPassword(e.target.value)}/>
                        </div>
                        <div className="input-remember">
                            <input type="checkbox" id="remember-me" />
                            <label>Remember me</label>
                        </div>
                        <button className="sign-in-button" type="submit" disabled={loginIsLoading}>{loginIsLoading ? 'Signing in...' : 'Sign in'}</button>
                    </form>
                    {loginIsError && <p className='perror'>{loginError.data.status} {loginError.data.message}</p>}
                    {loginIsSuccess && <><p className='psuccess'>{loginData.message} Token: {loginData.body.token}</p> <button onClick={handleProfile}>Get Profile</button></>}
    
                    {profileIsError && <p className='perror'>{profileError.data.status} {profileError.data.message}</p>}
                    {profileData && <p className='psuccess'>{profileData.message} First Name: {profileData.body.firstName} Last Name: {profileData.body.lastName} Surname: {profileData.body.userName}</p>}
    
                    
                </section>
            </div>
        );
    }
    
    Login or Signup to reply.
  2. Oftentimes you may need to persist some cached queries/mutations outside the API slice. Create an auth slice to hold the auth object reference. You’ll then be able to dispatch an action from the query/mutation to update the auth state using onQueryStarted, which can then be accessed in the base query function for the purpose of setting auth headers with stored token values.

    Example:

    auth.slice.js

    import { createSlice } from '@reduxjs/toolkit';
    
    const initialState = {
      token: null,
    };
    
    const authSlice = createSlice({
      name: "auth",
      initialState,
      reducers: {
        setAuthToken: (state, action) => {
          state.token = action.payload;
        },
      },
    });
    
    export const { setAuthToken } = authSlice.actions;
    
    export default authSlice.reducer;
    

    store.js

    import { configureStore } from '@reduxjs/toolkit';
    import { bankApi } from './ApiSlice.js';
    import authReducer from './auth.slice.js';
    
    export const store = configureStore({
      reducer: {
        [bankApi.reducerPath]: bankApi.reducer,
        auth: authReducer,
      },
      middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(bankApi.middleware),
    })
    

    api.slice.js

    import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
    import { setAuthToken } from './auth.slice.js';
    
    const apiBaseUrl = 'http://localhost:3001/api/v1';
    
    export const bankApi = createApi({
      reducerPath: 'bankApi',
      baseQuery: fetchBaseQuery({
        baseUrl: apiBaseUrl,
        prepareHeaders: (headers, { getState }) => {
          console.log('prepareHeaders is called');
          const token = getState().auth.token;
          if (token) {
            headers.set('Authorization', `Bearer ${token}`);
          }
          return headers;
        },
      }),
      endpoints: (builder) => ({
        auth: builder.mutation({
          query: (credentials) => ({
            url: '/user/login',
            method: 'POST',
            body: credentials,
          }),
          onQueryStarted: async (credentials, { dispatch, queryFulfilled }) => {
            try {
              const { data } = await queryFulfilled;
              dispatch(setAuthToken(data.body.token));
            } catch(error) {
              dispatch(setAuthToken(null));
            }
          },
        }),
        getProfile: builder.mutation({
          query: () => ({
            url: '/user/profile',
            method: 'POST',
          }),
        }),
        updateProfile: builder.query({
          query: () => ({
            url: '/user/profile',
            method: 'PUT',
          }),
        }),
      }),
    })
    
    export const {
      useAuthMutation,
      useGetProfileMutation,
      useUpdateProfileQuery
    } = bankApi;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search