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 ponents, 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:
- email error: .jpg
- password error: .jpg
- login successful: .jpg
- token missing from header after clicking get profile: .jpg
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 thegetProfile
query - Tried to pass the token to
useGetProfileMutation
inLoginpage.jsx
- Tried to store the token in
localStorage
and retrieve it to pass it touseGetProfileMutation
- Tried to wait for the login to be successful to then call
useGetProfileMutation
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 ponents, 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:
- email error: https://i.snipboard.io/PDRzNv.jpg
- password error: https://i.snipboard.io/vKZI2t.jpg
- login successful: https://i.snipboard.io/Jz84H1.jpg
- token missing from header after clicking get profile: https://i.snipboard.io/RedpYj.jpg
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 thegetProfile
query - Tried to pass the token to
useGetProfileMutation
inLoginpage.jsx
- Tried to store the token in
localStorage
and retrieve it to pass it touseGetProfileMutation
- Tried to wait for the login to be successful to then call
useGetProfileMutation
- Did you persist the auth response? – jifakir Commented Aug 6, 2023 at 3:58
2 Answers
Reset to default 5Oftentimes 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;
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, bineReducers } 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 = bineReducers({ // added bineReducer 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 ponents, 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>
);
}