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": "a@a.com",
"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:
prepareHeaders
prepareHeaders
in the getProfile
queryuseGetProfileMutation
in Loginpage.jsx
localStorage
and retrieve it to pass it to useGetProfileMutation
useGetProfileMutation
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;