I started learning RTK Query few days ago and I have been enjoying it cool features and simplicity, so I decided to switch from useContext to RTK Query in the project I’m building with Next.js and a custom server using Node.js and express. In this project, I made an api route for login and signup which would be hit by using RTK Query and Axios with the help of custom axios base query RTK Query provided. The login and signup api endpoints already had a logic to store token inside the cookies storage. I use RTK Query with axios to post user request so they can get a response of their token store in cookies storage. This logic of storing user token in the cookies works well with useContext and axios.
But the logic didnot work as expected while using RTK Query, and these are results:
- The token was set in the cookies storage but I get a response status of 401.
- When user submit their credentials in the login or signup page, they are supposed to be redirected to profile page with their details being display since I made use of useQuery to fetch user profile. But the data did not display. Which means the token stored is not effective.
- I’m unable to get the user information even though the token had been stored in the cookies.
- Whenever I click on a link to redirect me to a particular route, useQuery didnot fetch anything and when I go back to profile, the user details will be fetched and display but when I refresh the page again, no data will be dsiplay
- Whenever a get request was successful at the first time, I alway lose the data whenever I refresh the page.
- All these issues only happens to routes that are protected with middleware in the backend and the middleware is to verify the token. But I have no issue with reloading a page which data that is not protected in the backend.
- I also have a middleware in my backend for verifying and checking if token is true in the cookie to check if user is authenticated, if it is false, user should be directed to the login page in the frontend. The logic for fetching and check if data is true is inside HOC component which was wrapped with protected route, but whenever the data value is false, am still able to go to any route in the frontend instead of redirecting me to login page. And when I log the data to the console I recieve the correct data.
- Removing token from cookie works successfully.
Note: Backend code isrunning fine as it was tested with thunder client and everything works fine when using it with useContext.
Here is my login api route.
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
const router = require("express").Router();
const { body, validationResult } = require("express-validator");
const User = require("../../Model/UserSchema");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
router.post(
"/login",
[
body("email", "Email is required").isEmail(),
body("password", "At least 6 characters").exists(),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;
try {
const existingUser = await User.findOne({ email });
if (!existingUser)
return res
.status(401)
.json({ errors: [{ msg: "Wrong email or password" }] });
const passwordCorrect = await bcrypt.compare(
password,
existingUser.password
);
if (!passwordCorrect) {
return res.status(401).json({ msg: "Wrong email or password" });
}
const token = jwt.sign(
{ user: existingUser._id },
process.env.JWT_SECRET
);
//send cookies in httpOnly
res.cookie("token", token, {
httpOnly: true,
sameSite: "strict",
}).send;
res.status(200).send("Login Successful");
} catch (err) {
res.status(400).json("Bad Request");
}
}
);
module.exports = router;
The register api route is similar with the login route.
Here is the code for my middleware for storing cookies with user id
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
const jwt = require("jsonwebtoken");
function auth(req, res, next) {
try {
const token = req.cookies.token;
if (!token) {
return res.status(401).json({ msg: "Unauthorized" });
}
const verified = jwt.verify(token, process.env.JWT_SECRET);
req.user = verified.user;
next();
} catch (err) {
console.log(err);
res.status(401).res.json({ msg: Unauthorized });
}
}
<!-- begin snippet: js hide: false console: true babel: false -->
module.exports = auth;
Here is the middleware for checking if token is true in cookies
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
const jwt = require("jsonwebtoken");
const router = require("express").Router();
router.get("/auth", (req, res) => {
try {
const token = req.cookies.token;
if (!token) {
return res.json(false);
}
jwt.verify(token, process.env.JWT_SECRET);
res.send(true);
} catch (err) {
res.json(false);
}
});
module.exports = router;
Here is my AxiosBaseQuery and createApi
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import axios from "axios";
import { createApi } from "@reduxjs/toolkit/query/react";
const axiosBaseQuery =
({ baseUrl } = { baseUrl: "" }) =>
async ({ url, method, data }) => {
try {
const result = await axios({ url: baseUrl + url, method, data });
return { data: result.data };
} catch (axiosError) {
let err = axiosError;
return {
error: { status: err.response?.status, data: err.response?.data },
};
}
};
export const fetcherApi = createApi({
reducerPath: "fetcherApi",
baseQuery: axiosBaseQuery({
baseUrl: "http://localhost:5000/",
}),
tagTypes: ["User"],
endpoints(build) {
return {
//________Authentication
registerUser: build.mutation({
query: (form) => ({
url: "register",
method: "post",
data: form,
}),
invalidatesTags: ["User"],
}),
loginUser: build.mutation({
query: (form) => ({
url: "login",
method: "post",
data: form,
}),
invalidatesTags: ["User"],
}),
getAuth: build.query({
query: () => ({ url: "auth", method: "get" }),
}),
//__________User
updateUserName: build.mutation({
query: (...rest) => ({
url: "update-user",
method: "put",
data: rest,
}),
invalidatesTags: ["User"],
}),
getUser: build.query({
query: () => ({ url: "user", method: "get" }),
providesTags: ["User"],
}),
//__________Profile
postProfile: build.mutation({
query: (form) => ({
url: "login",
method: "post",
data: form,
}),
}),
getAllProfiles: build.query({
query: () => ({ url: "all-profiles", method: "get" }),
}),
getUserProfile: build.query({
query: () => ({ url: "profile/me", method: "get" }),
}),
//___________Car
postCar: build.mutation({
query: (form) => ({
url: "new-car",
method: "post",
data: form,
}),
}),
putCar: build.mutation({
query: ({ id, ...rest }) => ({
url: `update-car/{id}`,
method: "put",
data: { rest },
}),
}),
getAllCars: build.query({
query: () => ({ url: "all-cars", method: "get" }),
}),
getCarById: build.query({
query: (id) => ({ url: `onecar/${id}`, method: "get" }),
}),
getAllUserCars: build.query({
query: () => ({ url: "my-car", method: "get" }),
}),
};
},
});
export const {
// ______Authentication______
useGetAuthQuery,
useRegisterUserMutation,
useLoginUserMutation,
//_______User_________
useUpdateUserNameMutation,
useGetUserQuery,
//_____Profile_________
useGetUserProfileQuery,
useGetAllProfilesQuery,
usePostProfileMutation,
//_____Car____________
usePostCarMutation,
usePutCarMutation,
useGetAllCarsQuery,
useGetCarByIdQuery,
useGetAllUserCarsQuery,
} = fetcherApi;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import Layout from "@/components/Layout";
import Image from "next/image";
import { FaDollarSign } from "react-icons/fa";
import Link from "next/link";
import { FiEdit } from "react-icons/fi";
import Authorization from "@/HOC/Authorization";
import {
useGetUserProfileQuery,
useGetUserQuery,
} from "@/store/ReduxStore/fetcherApi";
import MyCars from "@/components/MyCars";
function Profile() {
const { data: profileData } = useGetUserProfileQuery();
const { data: userData } = useGetUserQuery();
return (
<Layout>
<div className="flex items-center justify-center mt-8 mx-auto w-4/5 ">
{/* Card code block start */}
<div className="bg-gray-300 shadow rounded-lg">
<div className="relative ">
<button className="w-full">
<div className="coverpicstwo">
<p className="text-xs text-gray-100">Change Cover Photo</p>
<div className="ml-2 text-gray-100">
<FiEdit />
</div>
</div>
<Image
className="h-1/6 shadow rounded-t w-full object-cover object-center"
src="/assets/images/d17.jpg"
alt="profile"
width={10000}
height={2000}
/>
</button>
<button className="roundedprofile">
<Image
className="w-full h-full z-10 overflow-hidden object-cover rounded-full"
src="/assets/images/d17.jpg"
alt="profile-image"
layout="fill"
/>
</button>
</div>
<div className="px-5 xl:px-10 pb-10 mt-2">
<div className="flex justify-center xl:justify-end w-full pt-16 xl:pt-5"></div>
<div className="pt-3 xl:pt-5 flex flex-col xl:flex-row items-start xl:items-center justify-between">
<div className="xl:pr-16 w-full xl:w-2/ mb-6 ">
<div className="profileidentity">
<h2 className="profilename">{userData?.name}</h2>
<div className="profilestatus">
{profileData ? (
<p>{profileData.businessStatus}</p>
) : (
<p>Buyer</p>
)}
</div>
</div>
<div className="profiledescription ">
{profileData ? (
<p>{profileData.description}</p>
) : (
<p>Setup description</p>
)}
</div>
</div>
<div className="workingstatusone">
<div className="workingstatustwo">
<Link href="/new-car" passHref>
<button className="workingstatusthree">
<span>
<FaDollarSign className="text-xl mr-1" />
</span>
Sell
</button>
</Link>
<div className="availablestatus">
{profileData ? (
<p>{profileData.availability}</p>
) : (
<p>Available</p>
)}
</div>
</div>
<div className="profilecontactbutton">
<Link
href="/profile-setup"
onClick={() => {
userNameHandler();
profileHandler();
}}
passHref
>
Contact || Edit Profile
</Link>
</div>
</div>
</div>
</div>
</div>
</div>
<MyCars />
</Layout>
);
}
export default Authorization(Profile);
Here is where profile can be edited. I made the previous profile as the current value, whenever the page is refresh the value will be lost.
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import Layout from "@/components/Layout";
import Authorization from "@/HOC/Authorization";
import {
useGetUserProfileQuery,
useGetUserQuery,
useUpdateUserNameMutation,
} from "@/store/ReduxStore/fetcherApi";
import { useState, useEffect } from "react";
import Image from "next/image";
import { FiEdit } from "react-icons/fi";
import PhoneInput from "react-phone-input-2";
import { useRouter } from "next/router";
import { usePostProfileMutation } from "@/store/ReduxStore/fetcherApi";
import Link from "next/link";
const ProfileSetup = () => {
const router = useRouter();
const [postProfile] = usePostProfileMutation();
const [updateUserName] = useUpdateUserNameMutation();
const { data: profile } = useGetUserProfileQuery();
const { data: user } = useGetUserQuery();
const [name, setName] = useState(user?.name);
const [description, setDescription] = useState(profile?.description);
const [businessStatus, setBusinessStatus] = useState(profile?.businessStatus);
const [availability, setAvailability] = useState(profile?.availability);
const [phoneNumber, setPhoneNumber] = useState(profile?.phoneNumber);
const form = {
description,
businessStatus,
availability,
phoneNumber,
};
const submitProfile = async (e) => {
e.preventDefault();
updateUserName(name);
postProfile(form);
router.push("/profile");
};
return (
<Layout>
<form onSubmit={submitProfile}>
<div className="bg-gray-100 ">
<div className="profilesetupheader">
<div className="profilesetupline">
<div className="flex w-11/12 mx-auto xl:w-full xl:mx-4 items-center">
<p className="setupheadertext">Profile Setup</p>
</div>
</div>
<div className="mx-auto">
<div className="xl:w-9/12 w-11/12 mx-auto xl:ml-8">
<div className="rounded relative mt-8 h-48">
<div className="coverpicsone" />
<div className="coverpicstwo">
<p className="text-xs text-gray-100">Change Cover Photo</p>
<div className="ml-2 text-gray-100">
<FiEdit />
</div>
</div>
<input
className="backgroundprofile"
type="file"
name="myImage"
accept="image/png, image/gif, image/jpeg"
// onChange={profileImageBackgroundUpload}
/>
<button className="profilepics">
<Image
src="/assets/images/d17.jpg"
alt="myprof"
className=" rounded-full"
layout="fill"
/>
<div className="profilepicsoverlay" />
<div className="cursor-pointer flex flex-col justify-center items-center z-10 text-gray-100">
<FiEdit />
<p className="text-xs text-gray-100">Edit Picture</p>
</div>
<input
className="profileimage"
type="file"
name="myImage"
accept="image/png, image/gif, image/jpeg, image/jpg"
/>
</button>
</div>
<div className="mt-16 flex flex-col xl:w-2/6 lg:w-1/2 md:w-1/2 w-full">
<label htmlFor="name" className="mylabel">
Name
</label>
<input
type="text"
name="name"
className="formname"
placeholder="John Doe"
onChange={(e) => setName(e.target.value)}
value={name}
required
/>
</div>
<div className="mt-8 flex flex-col xl:w-3/5 lg:w-1/2 md:w-1/2 w-full">
<label htmlFor="description" className="mylabel">
Description
</label>
<textarea
type="text"
name="description"
className="formdescription"
placeholder="Let the world know who you are"
onChange={(e) => setDescription(e.target.value)}
value={description}
maxLength="400"
required
/>
</div>
</div>
<div className="ml-10">
<div className="inline-flex justify-between py-4 space-x-20 ">
<div className=" flex flex-col xl:w-2/6 lg:w-1/2 md:w-1/2 w-full">
<label htmlFor="Buyer" className="mylabel">
Business Status
</label>
<select
name="businessStatus"
onChange={(e) => setBusinessStatus(e.target.value)}
value={businessStatus}
className="bg-gray-200 p-2"
>
<option>* Select Business Status</option>
<option value="buyer">Buyer</option>
<option value="seller">Seller</option>
</select>
</div>
</div>
<p className="mylabel">Available</p>
<select
name="availability"
value={availability}
onChange={(e) => setAvailability(e.target.value)}
className="bg-gray-200 p-2 "
>
<option>* Select Availability </option>
<option value="yes">Available</option>
<option value="no">Unavailable</option>
</select>
<div className="mt-6 mb-8 flex flex-col xl:w-2/6 lg:w-1/2 md:w-1/2 w-full pr-8">
<label htmlFor="phoneNumber" className="mylabel">
Phone Number
</label>
<PhoneInput
country={"us"}
name="phoneNumber"
onChange={phoneNumberChange}
value={phoneNumber}
/>
<p className="w-full text-left text-xs pt-1 text-gray-500 ">
You might choose not to include your phone number if this
seems like a security concern.
</p>
</div>
</div>
</div>
<div className="ml-10 justify-end">
<Link href="/profile" passHref>
<button className="profilecancelbutton">Cancel</button>
</Link>
<button
className="profilesubmitbutton"
type="submit"
onClick={submitProfile}
>
Save
</button>
</div>
</div>
</div>
</form>
</Layout>
);
};
export default Authorization(ProfileSetup);
Here is my HOC component for redirecting users that are not authenticated.
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import axios from "axios";
import { useGetAuthQuery } from "@/store/ReduxStore/fetcherApi";
import { setHttpAgentOptions } from "next/dist/server/config";
export default (ChildComponent) => {
const composeComponent = (props) => {
const router = useRouter();
const { data: authData } = useGetAuthQuery();
const [page, setPage] = useState(undefined);
useEffect(() => {
if (authData && authData === false) {
router.push("/login");
} else {
setPage(<ChildComponent {...props} />);
}
}, []);
return <div>{page}</div>;
};
return composeComponent;
};