tRPC is a toolkit that allows developers to statically type their API endpoints and share those types between the client and server, without installing extra libraries for code generation.

When we build full-stack applications with TypeScript, it becomes difficult to share types between the frontend and the backend.

Currently, GraphQL is the dominant library used to build and consume fully typesafe APIs in TypeScript. It offers a radically new approach to building type-safety APIs with more flexibility and control.

However, since GraphQL is a query language, it doesn’t leverage the full power of TypeScript and depends on code generation and already-defined schemas to implement typesafe APIs.

This is where tRPC was introduced to solve the complexities and flaws of GraphQL. tRPC allows us to build full-stack TypeScript applications and share the types directly between the client and server.

In this article, you’ll learn how to add JWT Authentication to your React.js, TypeScript, tRPC Client, tailwindCss and React Query projects.

tRPC Client and Server with React.js, Express, and Node.js Series:

  1. Build tRPC API with React.js, Node.js & MongoDB: Project Setup
  2. Build tRPC API with React.js & Node.js: Access and Refresh Tokens
  3. Full-Stack App tRPC, React.js, & Node.js: JWT Authentication

More practice:

Full-Stack App tRPC, React.js, & Node.js JWT Authentication

Prerequisites

Before we begin, you should:

  • Have basic knowledge of JavaScript, TypeScript, and React.js
  • Have basic knowledge of CSS and tailwindCss

React Query, tRPC Client, & React.js Overview

The user clicks on the “SignUp” button on the Homepage to create an account.

trpc client and server react home page

The user is taken to the signup page where he is required to provide his credentials.

trpc client and server react register new user with react query

Next, the user is redirected to the login page where he needs to provide his credentials to log in.

trpc client and server react login user with react query

After the tRPC Express server has successfully authenticated the user, React redirects the user to the profile page.

trpc client and server react profile page

You can inspect the “Application” tab in the dev tools to see the cookies returned by the tRPC Express server.

jwt refresh access token cookies in the browser

Setup React Query, tRPC Client, and React.js

We are going to use React.js, tRPC Client, and React Query along with the React binding for tRPC.

Follow the Project Setup article to set up the tRPC client and server with React, Node.js, Express, and tailwindCss before continuing with this article.

Now change the directory into the client folder cd packages/client and run this command to install the AppRouter type we exported from the tRPC server.


yarn add server@1.0.0
  • server – is the name used in the tRPC server package.json file.
  • @1.0.0 – is the version used in the tRPC server package.json file.

Managing State with Zustand

Redux is probably the most popular React state management library but it requires some amount of code to get it up and running with React. To update a single state, you will need to create actions, dispatch functions, and reducers for Redux to work properly.

Similar to React Context API, Redux needs to be wrapped with a provider component to make the Redux store available to any nested components that need to access the Redux store.

With Zustand, you can create a globally available state with few lines of code and inject it into any component, without the need to wrap a provider around your root app.

Another exciting feature of Zustand is that you can retrieve data from your state in functions that are not React components.

Before we start creating the React state with Zustand, create a packages/client/src/libs/types.ts file and add the following TypeScript types.

packages/client/src/libs/types.ts


export interface IUser {
  name: string;
  email: string;
  role: string;
  photo: string;
  _id: string;
  id: string;
  createdAt: string;
  updatedAt: string;
  __v: number;
}


Open your terminal and change the directory to the client folder cd packages/client and run this command to install the Zustand state management library:


yarn add zustand

Next, create a packages/client/src/store/index.ts file and add the following to set up the Zustand store for our application.

packages/client/src/store/index.ts


import create from 'zustand';
import { IUser } from '../libs/types';

type Store = {
  authUser: IUser | null;
  uploadingImage: boolean;
  pageLoading: boolean;
  setAuthUser: (user: IUser) => void;
  setUploadingImage: (isUploading: boolean) => void;
  setPageLoading: (isLoading: boolean) => void;
};

const useStore = create<Store>((set) => ({
  authUser: null,
  uploadingImage: false,
  pageLoading: false,
  setAuthUser: (user) => set((state) => ({ ...state, authUser: user })),
  setUploadingImage: (isUploading) =>
    set((state) => ({ ...state, uploadingImage: isUploading })),
  setPageLoading: (isLoading) =>
    set((state) => ({ ...state, pageLoading: isLoading })),
}));

export default useStore;

Create React Components with tailwindCss

Change the directory into the client folder cd packages/client and run this command to install these dependencies


yarn add react-router-dom tailwind-merge react-cookie

  • react-router-dom – a library that allows us to implement dynamic routing in React web apps.
  • tailwind-merge – a library to efficiently merge Tailwind CSS classes in JS without style conflicts.
  • react-cookie – a library that uses react hooks to access and modify cookies.

Create a Header Component with tailwindCss

packages/client/src/components/Header.tsx


import { useQueryClient } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import useStore from "../store";
import { trpc } from "../trpc";

const Header = () => {
  const store = useStore();
  const user = store.authUser;

  const queryClient = useQueryClient();
  const { mutate: logoutUser } = trpc.logoutUser.useMutation({
    onSuccess(data) {
      queryClient.clear();
      document.location.href = "/login";
    },
    onError(error) {
      queryClient.clear();
      document.location.href = "/login";
    },
  });

  const handleLogout = () => {
    logoutUser();
  };

  return (
    <header className="bg-white h-20">
      <nav className="h-full flex justify-between container items-center">
        <div>
          <Link to="/" className="text-ct-dark-600 text-2xl font-semibold">
            CodevoWeb
          </Link>
        </div>
        <ul className="flex items-center gap-4">
          <li>
            <Link to="/" className="text-ct-dark-600">
              Home
            </Link>
          </li>
          {!user && (
            <>
              <li>
                <Link to="/register" className="text-ct-dark-600">
                  SignUp
                </Link>
              </li>
              <li>
                <Link to="/login" className="text-ct-dark-600">
                  Login
                </Link>
              </li>
            </>
          )}
          {user && (
            <>
              <li>
                <Link to="/profile" className="text-ct-dark-600">
                  Profile
                </Link>
              </li>
              <li className="cursor-pointer">Create Post</li>
              <li className="cursor-pointer" onClick={handleLogout}>
                Logout
              </li>
            </>
          )}
        </ul>
      </nav>
    </header>
  );
};

export default Header;

In the above code, we evoked the useMutation() hook provided by React Query to make a tRPC mutation request to the tRPC server in order to log out the user.

Create a Loading Spinner with tailwindCss

packages/client/src/components/Spinner.tsx


import React from 'react';
import { twMerge } from 'tailwind-merge';
type SpinnerProps = {
  width?: number;
  height?: number;
  color?: string;
  bgColor?: string;
};
const Spinner: React.FC<SpinnerProps> = ({
  width = 5,
  height = 5,
  color,
  bgColor,
}) => {
  return (
    <svg
      role='status'
      className={twMerge(
        'w-5 h-5 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600',
        `w-${width} h-${height} ${color} ${bgColor}`
      )}
      viewBox='0 0 100 101'
      fill='none'
      xmlns='http://www.w3.org/2000/svg'
    >
      <path
        d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
        fill='currentColor'
      />
      <path
        d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
        fill='currentFill'
      />
    </svg>
  );
};

export default Spinner;

Create a Screen Loader with React and tailwindCss

packages/client/src/components/FullScreenLoader.tsx


import Spinner from './Spinner';

const FullScreenLoader = () => {
  return (
    <div className='w-screen h-screen fixed'>
      <div className='absolute top-64 left-1/2 -translate-x-1/2'>
        <Spinner width={8} height={8} />
      </div>
    </div>
  );
};

export default FullScreenLoader;

Create a Loading Button with React and tailwindCss

packages/client/src/components/LoadingButton.tsx


import React from 'react';
import Spinner from './Spinner';

type LoadingButtonProps = {
  loading: boolean;
  btnColor?: string;
  textColor?: string;
  children: React.ReactNode;
};

export const LoadingButton: React.FC<LoadingButtonProps> = ({
  textColor = 'text-white',
  btnColor = 'bg-ct-yellow-600',
  children,
  loading = false,
}) => {
  return (
    <button
      type='submit'
      className={`w-full py-3 font-semibold ${btnColor} rounded-lg outline-none border-none flex justify-center ${
        loading ? 'bg-[#ccc]' : ''
      }`}
    >
      {loading ? (
        <div className='flex items-center gap-3'>
          <Spinner />
          <span className='text-slate-500'>Loading...</span>
        </div>
      ) : (
        <span className={`${textColor}`}>{children}</span>
      )}
    </button>
  );
};

React Layout Component with React-Router-Dom

packages/client/src/components/Layout.tsx


import { Outlet } from 'react-router-dom';
import Header from './Header';

const Layout = () => {
  return (
    <>
      <Header />
      <Outlet />
    </>
  );
};

export default Layout;

Create an InputField with React-Hook-Form and TailwindCSS


yarn add react-hook-form zod @hookform/resolvers

  • react-hook-form – a library for validating forms in React.js
  • zod – a TypeScript-first schema declaration and validation library

packages/client/src/components/FormInput.tsx


import React from 'react';
import { useFormContext } from 'react-hook-form';

type FormInputProps = {
  label: string;
  name: string;
  type?: string;
};

const FormInput: React.FC<FormInputProps> = ({
  label,
  name,
  type = 'text',
}) => {
  const {
    register,
    formState: { errors },
  } = useFormContext();
  return (
    <div className=''>
      <label htmlFor={name} className='block text-ct-blue-600 mb-3'>
        {label}
      </label>
      <input
        type={type}
        placeholder=' '
        className='block w-full rounded-2xl appearance-none focus:outline-none py-2 px-4'
        {...register(name)}
      />
      {errors[name] && (
        <span className='text-red-500 text-xs pt-1 block'>
          {errors[name]?.message as string}
        </span>
      )}
    </div>
  );
};

export default FormInput;

Create an Image Uploader with React and Cloudinary

To begin, register for an account on Cloudinary and follow the following steps to configure the upload preset on your account.

Step 1: Click on the Settings icon on the top navigation bar.

cloudinary dashboard

Step 2: Click the “Upload” tab on the settings page

cloudinary dashboard upload tab

Step 3: Scroll down to the “Upload presets” section and click on the “Add upload preset” link

cloudinary dashboard upload settings

Step 4: Enter the “Upload preset name” and change the “Signing Mode” to “Unsigned“. Input the preset name as the folder name.

Once you are done, click on the “Upload Manipulations” tab on the left sidebar.

cloudinary dashboard upload settings preset

Step 5: Click on the “Edit” link under the “Incoming Transformation” section.

cloudinary dashboard add upload preset

Step 6: Change the width and height to your preferred size and change the quality type from “Manual” to “Automatic – best quality“.

Once you are done, click the “ok” button.

cloudinary dashboard traform image

Step 7: Click on the “Save” button to save the changes.

cloudinary dashboard click on the save button

For security reasons don’t expose your “upload preset name” to the public to avoid unsolicited upload of images by users.

I will definitely change my account details at the end of this tutorial.

Now create packages/client/src/components/FileUpload.tsx file and add the following code to process the upload request.

packages/client/src/components/FileUpload.tsx


import React, { useCallback } from 'react';
import { Controller, useController, useFormContext } from 'react-hook-form';
import useStore from '../store';
import Spinner from './Spinner';

const CLOUDINARY_UPLOAD_PRESET = 'trpc-api';
const CLOUDINARY_URL = 'https://api.cloudinary.com/v1_1/Codevo/image/upload';

type FileUpLoaderProps = {
  name: string;
};
const FileUpLoader: React.FC<FileUpLoaderProps> = ({ name }) => {
  const {
    control,
    formState: { errors },
  } = useFormContext();
  const { field } = useController({ name, control });
  const store = useStore();

  const onFileDrop = useCallback(
    async (e: React.SyntheticEvent<EventTarget>) => {
      const target = e.target as HTMLInputElement;
      if (!target.files) return;
      const newFile = Object.values(target.files).map((file: File) => file);
      const formData = new FormData();
      formData.append('file', newFile[0]);
      formData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);

      store.setUploadingImage(true);
      const data = await fetch(CLOUDINARY_URL, {
        method: 'POST',
        body: formData,
      })
        .then((res) => {
          store.setUploadingImage(false);

          return res.json();
        })
        .catch((err) => {
          store.setUploadingImage(false);
          console.log(err);
        });

      if (data.secure_url) {
        field.onChange(data.secure_url);
      }
    },

    [field, store]
  );

  return (
    <Controller
      name={name}
      defaultValue=''
      control={control}
      render={({ field: { name, onBlur, ref } }) => (
        <>
          <div className='mb-2 flex justify-between items-center'>
            <div>
              <span className='block mb-2'>Choose profile photo</span>
              <input
                className='block text-sm mb-2 text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-violet-50 file:text-violet-700 hover:file:bg-violet-100'
                type='file'
                name={name}
                onBlur={onBlur}
                ref={ref}
                onChange={onFileDrop}
                multiple={false}
                accept='image/jpg, image/png, image/jpeg'
              />
            </div>
            <div>
              {store.uploadingImage && <Spinner color='text-yellow-400' />}
            </div>
          </div>
          <p
            className={`text-red-500 text-xs italic mb-2 ${
              errors[name] ? 'visible' : 'invisible'
            }`}
          >
            {errors[name] && (errors[name]?.message as string)}
          </p>
        </>
      )}
    />
  );
};

export default FileUpLoader;

From the above code, you will need two pieces of information, the Cloudinary RESTful image upload URL and your preset name.

For example, to upload an image to your account, you would have to make a POST request with the formData to:

POST https://api.cloudinary.com/v1_1/:cloud_name/image/upload

Where :cloud_name is your Cloudinary Cloud name. Also, you need to append your upload preset name to the formData.

React Query and tRPC Client: Register User

Change the directory to the client folder cd packages/client and install this package to allow us to show alert notifications.


yarn add react-toastify

With that out of the way, let’s call the evoke the registerUser procedure to add the new user to the database.

packages/client/src/pages/register.page.tsx


import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { object, string, TypeOf } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import { trpc } from "../trpc";
import FileUpLoader from "../components/FileUpload";

const registerSchema = object({
  name: string().min(1, "Full name is required").max(100),
  email: string()
    .min(1, "Email address is required")
    .email("Email Address is invalid"),
  photo: string().min(1, "Photo is required").url("Photo URL is invalid"),
  password: string()
    .min(1, "Password is required")
    .min(8, "Password must be more than 8 characters")
    .max(32, "Password must be less than 32 characters"),
  passwordConfirm: string().min(1, "Please confirm your password"),
}).refine((data) => data.password === data.passwordConfirm, {
  path: ["passwordConfirm"],
  message: "Passwords do not match",
});

export type RegisterInput = TypeOf<typeof registerSchema>;

const RegisterPage = () => {
  const navigate = useNavigate();

  const { isLoading, mutate: SignUpUser } = trpc.registerUser.useMutation({
    onSuccess: (data) => {
      toast.success("Registration successful");
      navigate("/login");
    },
    onError: (error) => {
      toast.error(error.message, {
        type: "error",
        position: "top-right",
      });
    },
  });

  const methods = useForm<RegisterInput>({
    resolver: zodResolver(registerSchema),
  });

  const {
    reset,
    handleSubmit,
    formState: { isSubmitSuccessful },
  } = methods;

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  const onSubmitHandler: SubmitHandler<RegisterInput> = (values) => {
    // 👇 Execute the Mutation
    SignUpUser(values);
  };

  return (
    <section className="py-8 bg-ct-blue-600 min-h-screen grid place-items-center">
      <div className="w-full">
        <h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
          Welcome to CodevoWeb!
        </h1>
        <h2 className="text-lg text-center mb-4 text-ct-dark-200">
          Sign Up To Get Started!
        </h2>
        <FormProvider {...methods}>
          <form
            onSubmit={handleSubmit(onSubmitHandler)}
            className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
          >
            <FormInput label="Full Name" name="name" />
            <FormInput label="Email" name="email" type="email" />
            <FormInput label="Password" name="password" type="password" />
            <FormInput
              label="Confirm Password"
              name="passwordConfirm"
              type="password"
            />
            <FileUpLoader name="photo" />
            <span className="block">
              Already have an account?{" "}
              <Link to="/login" className="text-ct-blue-600">
                Login Here
              </Link>
            </span>
            <LoadingButton loading={isLoading} textColor="text-ct-blue-600">
              Sign Up
            </LoadingButton>
          </form>
        </FormProvider>
      </div>
    </section>
  );
};

export default RegisterPage;

React Query and tRPC Client: Login User

packages/client/src/pages/login.page.tsx


import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { object, string, TypeOf } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import FormInput from "../components/FormInput";
import { LoadingButton } from "../components/LoadingButton";
import { toast } from "react-toastify";
import { trpc } from "../trpc";

const loginSchema = object({
  email: string()
    .min(1, "Email address is required")
    .email("Email Address is invalid"),
  password: string()
    .min(1, "Password is required")
    .min(8, "Password must be more than 8 characters")
    .max(32, "Password must be less than 32 characters"),
});

export type LoginInput = TypeOf<typeof loginSchema>;

const LoginPage = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const from = ((location.state as any)?.from.pathname as string) || "/profile";

  const { isLoading, mutate: loginUser } = trpc.loginUser.useMutation({
    onSuccess(data) {
      toast("Logged in successfully", {
        type: "success",
        position: "top-right",
      });
      navigate(from);
    },
    onError(error) {
      toast(error.message, {
        type: "error",
        position: "top-right",
      });
    },
  });

  const methods = useForm<LoginInput>({
    resolver: zodResolver(loginSchema),
  });

  const {
    reset,
    handleSubmit,
    formState: { isSubmitSuccessful },
  } = methods;

  useEffect(() => {
    if (isSubmitSuccessful) {
      reset();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSubmitSuccessful]);

  const onSubmitHandler: SubmitHandler<LoginInput> = (values) => {
    // 👇 Executing the loginUser Mutation
    loginUser(values);
  };

  return (
    <section className="bg-ct-blue-600 min-h-screen grid place-items-center">
      <div className="w-full">
        <h1 className="text-4xl xl:text-6xl text-center font-[600] text-ct-yellow-600 mb-4">
          Welcome Back
        </h1>
        <h2 className="text-lg text-center mb-4 text-ct-dark-200">
          Login to have access
        </h2>
        <FormProvider {...methods}>
          <form
            onSubmit={handleSubmit(onSubmitHandler)}
            className="max-w-md w-full mx-auto overflow-hidden shadow-lg bg-ct-dark-200 rounded-2xl p-8 space-y-5"
          >
            <FormInput label="Email" name="email" type="email" />
            <FormInput label="Password" name="password" type="password" />

            <div className="text-right">
              <Link to="#" className="">
                Forgot Password?
              </Link>
            </div>
            <LoadingButton loading={isLoading} textColor="text-ct-blue-600">
              Login
            </LoadingButton>
            <span className="block">
              Need an account?{" "}
              <Link to="/register" className="text-ct-blue-600">
                Sign Up Here
              </Link>
            </span>
          </form>
        </FormProvider>
      </div>
    </section>
  );
};

export default LoginPage;

React Query and tRPC Client: Authentication Guard

On the tRPC server, we returned a logged_in cookie that is not HTTPOnly to the client or browser. This will enable us to access it from the React application.

Now let’s create an authentication guard to protect the private routes on the React application.

The requireUser auth guard will make a tRPC query request to retrieve the authenticated user’s information.

This request will only be successful if the user is logged in. When the request results in an error, we immediately redirect the user to the login page.

packages/client/src/components/requireUser.tsx


import { useCookies } from "react-cookie";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { IUser } from "../libs/types";
import useStore from "../store";
import { trpc } from "../trpc";
import FullScreenLoader from "./FullScreenLoader";

const RequireUser = ({ allowedRoles }: { allowedRoles: string[] }) => {
  const [cookies] = useCookies(["logged_in"]);
  const location = useLocation();
  const store = useStore();

  const {
    isLoading,
    isFetching,
    data: user,
  } = trpc.getMe.useQuery(undefined, {
    retry: 1,
    select: (data) => data.data.user,
    onSuccess: (data) => {
      store.setAuthUser(data as IUser);
    },
    onError: (error) => {
      console.log(error);
      if (error.message.includes("Could not refresh access token")) {
        document.location.href = "/login";
      }
    },
  });

  const loading = isLoading || isFetching;

  if (loading) {
    return <FullScreenLoader />;
  }

  return (cookies.logged_in || user) &&
    allowedRoles.includes(user?.role as string) ? (
    <Outlet />
  ) : cookies.logged_in && user ? (
    <Navigate to="/unauthorized" state={{ from: location }} replace />
  ) : (
    <Navigate to="/login" state={{ from: location }} replace />
  );
};

export default RequireUser;

React Query and tRPC Client: Middleware Guard

packages/client/src/middleware/AuthMiddleware.tsx


import { useCookies } from "react-cookie";
import FullScreenLoader from "../components/FullScreenLoader";
import React from "react";
import { trpc } from "../trpc";
import { IUser } from "../libs/types";
import { useQueryClient } from "@tanstack/react-query";
import useStore from "../store";

type AuthMiddlewareProps = {
  children: React.ReactElement;
};

const AuthMiddleware: React.FC<AuthMiddlewareProps> = ({ children }) => {
  const [cookies] = useCookies(["logged_in"]);
  const store = useStore();

  const queryClient = useQueryClient();
  const { refetch } = trpc.refreshToken.useQuery(undefined, {
    retry: 1,
    enabled: false,
    onSuccess: (data) => {
      queryClient.invalidateQueries([["getMe"]]);
    },
  });

  const query = trpc.getMe.useQuery(undefined, {
    enabled: !!cookies.logged_in,
    retry: 1,
    select: (data) => data.data.user,
    onSuccess: (data) => {
      store.setAuthUser(data as IUser);
    },
    onError: (error) => {
      let retryRequest = true;
      if (error.message.includes("must be logged in") && retryRequest) {
        retryRequest = false;
        try {
          refetch({ throwOnError: true });
        } catch (err: any) {
          console.log(err);
          if (err.message.includes("Could not refresh access token")) {
            document.location.href = "/login";
          }
        }
      }
    },
  });

  if (query.isLoading && cookies.logged_in) {
    return <FullScreenLoader />;
  }

  return children;
};

export default AuthMiddleware;

Creating the Home and Profile Pages with tailwindCss

Home Page


const HomePage = () => {
  return (
    <section className='bg-ct-blue-600 min-h-screen pt-20'>
      <div className='max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center'>
        <p className='text-5xl font-semibold'>Home Page</p>
      </div>
    </section>
  );
};

export default HomePage;

Profile Page


import useStore from '../store';

const ProfilePage = () => {
  const store = useStore();

  const user = store.authUser;

  return (
    <section className='bg-ct-blue-600 min-h-screen pt-20'>
      <div className='max-w-4xl mx-auto bg-ct-dark-100 rounded-md h-[20rem] flex justify-center items-center'>
        <div>
          <p className='text-5xl font-semibold'>Profile Page</p>
          <div className='mt-8'>
            <p className='mb-4'>ID: {user?.id}</p>
            <p className='mb-4'>Name: {user?.name}</p>
            <p className='mb-4'>Email: {user?.email}</p>
            <p className='mb-4'>Role: {user?.role}</p>
          </div>
        </div>
      </div>
    </section>
  );
};

export default ProfilePage;

Setup Routing with React-Router-Dom

packages/client/src/router/index.tsx


import { Suspense, lazy } from 'react';
import type { RouteObject } from 'react-router-dom';
import FullScreenLoader from '../components/FullScreenLoader';
import Layout from '../components/Layout';
import RequireUser from '../components/requireUser';
import HomePage from '../pages/home.page';
import LoginPage from '../pages/login.page';
import ProfilePage from '../pages/profile.page';

const Loadable =
  (Component: React.ComponentType<any>) => (props: JSX.IntrinsicAttributes) =>
    (
      <Suspense fallback={<FullScreenLoader />}>
        <Component {...props} />
      </Suspense>
    );

const RegisterPage = Loadable(lazy(() => import('../pages/register.page')));

const authRoutes: RouteObject = {
  path: '*',
  children: [
    {
      path: 'login',
      element: <LoginPage />,
    },
    {
      path: 'register',
      element: <RegisterPage />,
    },
  ],
};

const normalRoutes: RouteObject = {
  path: '*',
  element: <Layout />,
  children: [
    {
      index: true,
      element: <HomePage />,
    },
    {
      path: 'profile',
      element: <RequireUser allowedRoles={['user']} />,
      children: [{ path: '', element: <ProfilePage /> }],
    },
  ],
};

const routes: RouteObject[] = [authRoutes, normalRoutes];

export default routes;

Update the Index and App Files

packages/client/src/App.tsx


import { useState } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useRoutes } from "react-router-dom";
import { getFetch, httpBatchLink, loggerLink } from "@trpc/client";
import routes from "./router";
import { trpc } from "./trpc";
import AuthMiddleware from "./middleware/AuthMiddleware";

function AppContent() {
  const content = useRoutes(routes);
  return content;
}

function App() {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 5 * 1000,
          },
        },
      })
  );

  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        loggerLink(),
        httpBatchLink({
          url: "http://localhost:8000/api/trpc",
          fetch: async (input, init?) => {
            const fetch = getFetch();
            return fetch(input, {
              ...init,
              credentials: "include",
            });
          },
        }),
      ],
    })
  );
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <AuthMiddleware>
          <AppContent />
        </AuthMiddleware>
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

export default App;

packages/client/src/index.tsx


import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import App from './App';

import './global.css';
import 'react-toastify/dist/ReactToastify.css';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Router>
      <App />
      <ToastContainer />
    </Router>
  </React.StrictMode>
);

Conclusion

With this React Query, tRPC Client, tailwindCss, and React-Hook-Form example in Typescript, you’ve learned how to add access and refresh token functionality to your React.js applications.

React Query and tRPC Client Source Code

Check out the complete source code for: