GraphQL-CodeGen is a tool for generating GraphQL requests. This article will teach you how to implement access and refresh token functionality with React Query, graphql-request, React-Hook-Form, and Zod.

More practice:

React Query, and GraphQL-CodeGen Access, and Refresh Tokens

React Query and GraphQL-CodeGen Overview

React Query, & GraphQL-CodeGen ...
React Query, & GraphQL-CodeGen Access, and Refresh Tokens

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

react query and graphql codegen home page

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

react query and graphql codegen register user mutation

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

react query and graphql codegen login user mutation

After the user has been successfully authenticated by the GraphQL server, React then redirects the user to the profile page.

react query and graphql codegen profile page

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

jwt refresh access token cookies in the browser

Setup React Query and GraphQL-CodeGen in React

First and foremost, run the following command in your terminal to create a React boilerplate application in a react-query-graphql directory.


yarn create react-app react-query-graphql --template typescript
# or
npx create-react-app react-query-graphql --template typescript

Running this command will install and execute the React project scaffolding tool (Create-React-App).

Setup React Query

After the React boilerplate project has been generated, run this command to install React Query:


yarn add @tanstack/react-query @tanstack/react-query-devtools
# or
npm install @tanstack/react-query @tanstack/react-query-devtools

Note: React Query is compatible with React v16.8+ and works well with ReactDOM and React Native.

To make the query context available to the entire app, we need to wrap the QueryClientProvider component around the root app and provide it with the QueryClient instance.


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 10 * 1000,
    },
  },
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <react.strictmode>
    <queryclientprovider client="{queryClient}">
      <app />
      <reactquerydevtools initialisopen="{false}" />
   </queryclientprovider>
  </react.strictmode>
);

Install GraphQL and GraphQL-Request

graphql-request is a minimalist GraphQL client that is kind of similar to Axios but tailored specifically for making GraphQL requests.


yarn add graphql graphql-request
# or
npm install graphql graphql-request

GraphQL-CodeGen Manual Setup

GraphQL-CodeGen is a tool designed for generating code from GraphQL schema and operations.

Run this command to install the GraphQL CodeGen CLI tool to enable us to generate the GraphQL request hooks directly from the terminal.


yarn add -D @graphql-codegen/cli
# or 
npm install -D @graphql-codegen/cli

Before CodeGen can auto-generate the typed React Query hook for each GraphQL operation, we need to install these required plugins.


yarn add -D @graphql-codegen/typescript-operations @graphql-codegen/typescript @graphql-codegen/typescript-react-query
# or 
npm install -D @graphql-codegen/typescript-operations @graphql-codegen/typescript @graphql-codegen/typescript-react-query

Next, let’s create a codegen.yml file with some configurations to tell CodeGen how we want the hooks to be generated.

codegen.yml


schema: http://localhost:8000/graphql
documents: './src/**/*.graphql'
generates:
  ./src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-query
    config:
      fetcher: graphql-request
  • schema – the schema field is simply the URL to your Graphql gateway backend or the path to a .schema file in your project directory.
  • documents – the documents field tells CodeGen where to look for the GraphQL files in order to generate the hooks.
  • generates – the generates field tells CodeGen where to output the generated Typescript types, React Query mutations, and queries.
  • plugins – the plugins field contains a list of plugins we want CodeGen to use to generate the Typescript types and React Query hooks.
  • fetcher the fetcher field tells CodeGen the GraphQL client we want to use to make the requests.

Next, let’s add the GraphQL CodeGen script to the package.json file and provide it the path to the codegen.yml file.

package.json


{
"scripts": {
    "generate": "graphql-codegen --config codegen.yml"
  }
}

Define the GraphQL Mutations and Queries

Mutations allow us to modify server data and it can be used to perform insert, update and delete operations.

Queries on the other hand allow us to fetch data from the GrapQL server.

To define a mutation or query, you need to specify the operation type (query, mutation, or subscription) and the operation name.

  • The operation type describes the type of GraphQL operation you are intending to do.
  • The operation name is a meaningful and optional name for your operation. You need to specify the operation name for each request because GraphQL CodeGen will use it to define the React Query hook and the query key.

Register User Mutation

src/graphql/SignUpMutation.graphql


mutation SignUpUser($input: SignUpInput!) {
  signupUser(input: $input) {
    status
    user {
      name
      email
      photo
      role
    }
  }
}

Login User Mutation

src/graphql/LoginMutation.graphql


mutation LoginUser($input: LoginInput!) {
  loginUser(input: $input) {
    status
    access_token
  }
}

Get Logged-in User’s Credentials Query

src/graphql/GetMeQuery.graphql


query GetMe {
  getMe {
    status
    user {
      id
      email
      name
      role
      photo
      updatedAt
      createdAt
    }
  }
}

Refresh Access Token Query

src/graphql/RefreshAccessTokenQuery.graphql


query RefreshAccessToken {
  refreshAccessToken {
    status
    access_token
  }
}

Logout User Query

src/graphql/LogoutQuery.graphql


query LogoutUser {
  logoutUser
}

Auto-Generate the Typescript Types and React Query Hooks

Now let’s run the generate script we added to the package.json file so that CodeGen can generate both the Typescript types and the React Query hooks.


yarn generate
# or 
npm run generate

After the generation is completed, you should see a newly-created ./src/generated/graphql.ts file containing all the Typescript types, and React Query hooks.

Setup tailwindCss in React

Install tailwindCss and its dependencies

To begin, let’s install tailwindCss and its peer dependencies via npm or yarn.


npm install -D tailwindcss postcss autoprefixer
# or
yarn add -D tailwindcss postcss autoprefixer

Run the init command in the terminal to generate both tailwind.config.js and postcss.config.js files.


npx tailwindcss init -p

Configure your template paths

Next, add the paths to your template files in the tailwind.config.js file, and feel free to also add your custom colors and fonts.

tailwind.config.js


/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {
      colors: {
        'ct-dark-600': '#222',
        'ct-dark-200': '#e5e7eb',
        'ct-dark-100': '#f5f6f7',
        'ct-blue-600': '#2363eb',
        'ct-yellow-600': '#f9d13e',
      },
      fontFamily: {
        Poppins: ['Poppins, sans-serif'],
      },
      container: {
        center: true,
        padding: '1rem',
        screens: {
          lg: '1125px',
          xl: '1125px',
          '2xl': '1125px',
        },
      },
    },
  },
  plugins: [],
};

Add the tailwindCss directives to your CSS

Next, create a ./src/index.css file and add the @tailwind directives.


@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;

Import the CSS file

Now, import the ./src/index.css file into the ./src/index.tsx file.

Create the GraphQL Client

.env.local


REACT_APP_GRAPHQL_ENDPOINT=http://localhost:8000/graphql

src/clients/graphqlRequestClient.ts


import { GraphQLClient } from 'graphql-request';

const GRAPHQL_ENDPOINT = process.env.REACT_APP_GRAPHQL_ENDPOINT as string;

const graphqlRequestClient = new GraphQLClient(GRAPHQL_ENDPOINT, {
  credentials: 'include',
  mode: 'cors',
});

export default graphqlRequestClient;

Note: You need to set credentials: 'include' for graphql-request to include the cookies along with the requests.

Create Global State with React Context API

Now to avoid adding more complexity to this project by adding Redux or any other state manager, let’s use React Context API to define a global state where we can pass data through the component tree without having to pass props down manually at every level

src/context/types.ts


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

At this stage, we will only store the authenticated user’s credentials so that we can display them on the profile page.

src/context/index.tsx


import React from 'react';
import { IUser } from './types';

type State = {
  authUser: IUser | null;
};

type Action = {
  type: string;
  payload: IUser | null;
};

type Dispatch = (action: Action) => void;

const initialState: State = {
  authUser: null,
};

type StateContextProviderProps = { children: React.ReactNode };

const StateContext = React.createContext<
  { state: State; dispatch: Dispatch } | undefined
>(undefined);

const stateReducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'SET_USER': {
      return {
        ...state,
        authUser: action.payload,
      };
    }
    default: {
      throw new Error(`Unhandled action type`);
    }
  }
};

const StateContextProvider = ({ children }: StateContextProviderProps) => {
  const [state, dispatch] = React.useReducer(stateReducer, initialState);
  const value = { state, dispatch };
  return (
    <StateContext.Provider value={value}>{children}</StateContext.Provider>
  );
};

const useStateContext = () => {
  const context = React.useContext(StateContext);

  if (context) {
    return context;
  }

  throw new Error(`useStateContext must be used within a StateContextProvider`);
};

export { StateContextProvider, useStateContext };

Create Reusable React Components with tailwindCss

src/components/Header.tsx


import { useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { toast } from 'react-toastify';
import graphqlRequestClient from '../clients/graphqlRequestClient';
import { useStateContext } from '../context';
import { LogoutUserQuery, useLogoutUserQuery } from '../generated/graphql';

const Header = () => {
  const stateContext = useStateContext();
  const user = stateContext.state.authUser;

  const queryClient = useQueryClient();
  const { refetch } = useLogoutUserQuery(
    graphqlRequestClient,
    {},
    {
      enabled: false,
      onSuccess(data: LogoutUserQuery) {
        queryClient.clear();
        document.location.href = '/login';
      },
      onError(error: any) {
        error.response.errors.forEach((err: any) => {
          toast(err.message, {
            type: 'error',
            position: 'top-right',
          });
          queryClient.clear();
          document.location.href = '/login';
        });
      },
    }
  );

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

  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;

src/components/Spinner.tsx


import React from 'react';
type SpinnerProps = {
  width: number;
  height: number;
};
const Spinner: React.FC<SpinnerProps> = ({ width, height }) => {
  return (
    <svg
      role='status'
      className={`w-${width} h-${height} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
      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;

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;

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 width={5} height={5} />
          <span className='text-slate-500'>Loading...</span>
        </div>
      ) : (
        <span className={`${textColor}`}>{children}</span>
      )}
    </button>
  );
};

React Layout Component with React-Router-Dom Outlet


yarn add react-router-dom
# or 
npm install react-router-dom

src/components/Layout.tsx


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

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

export default Layout;

React TailwindCSS Input Field with React-Hook-Form


yarn add react-hook-form zod @hookform/resolvers
# or 
npm install react-hook-form zod @hookform/resolvers

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 unknown as string}
        </span>
      )}
    </div>
  );
};

export default FormInput;

React Query and GraphQL Request: Register User

To modify the server data, we use the React Query mutation hook generated by the GraphQL CodeGen tool.


const { isLoading,isError, isSuccess, error, mutate: loginUser } = useLoginUserMutation<Error>(
    graphqlRequestClient,
    {
      onSuccess(data: LoginUserMutation) {},
      onError(error: any) {},
      retry: false,
    }
  );
  • useLoginUserMutation : is the React Query mutation hook generated by CodeGen.
  • graphqlRequestClient : is the GraphQL client instance exported from ./src/clients/graphqlRequestClient.ts file.
  • onSuccess : a function that will be evoked when the mutation resolves successfully and receives the data returned by graphql-request as a parameter.
  • onError : a function that will be evoked when the mutation resolves in an error.
  • mutate : a function to manually execute the mutation

You can read more about the useMutation() hook on the React Query official website.

Before we write the code to register a new user, let’s install this package to show some alert notifications.


yarn add react-toastify
# or 
npm install react-toastify

With that out of the way, let’s use the mutation hook and the graphql-request client to add a new user to the database.

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 {
  SignUpUserMutation,
  useSignUpUserMutation,
} from '../generated/graphql';
import graphqlRequestClient from '../clients/graphqlRequestClient';
import FormInput from '../components/FormInput';
import { LoadingButton } from '../components/LoadingButton';
import { toast } from 'react-toastify';

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'),
  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 { mutate: SignUpUser, isLoading } = useSignUpUserMutation<Error>(
    graphqlRequestClient,
    {
      onSuccess(data: SignUpUserMutation) {
        console.log(data.signupUser.user);
        navigate('/login');
      },
      onError(error: any) {
        error.response.errors.forEach((err: any) => {
          toast(err.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({ input: 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'
            />
            <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 GraphQL Request: Login User

Once we are able to register a user, let’s write the code to sign in the registered user.

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 {
  LoginUserMutation,
  useGetMeQuery,
  useLoginUserMutation,
} from '../generated/graphql';
import graphqlRequestClient from '../clients/graphqlRequestClient';
import { useStateContext } from '../context';
import { IUser } from '../context/types';
import { toast } from 'react-toastify';

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 stateContext = useStateContext();

  const query = useGetMeQuery(
    graphqlRequestClient,
    {},
    {
      enabled: false,
      onSuccess: (data) => {
        stateContext.dispatch({
          type: 'SET_USER',
          payload: data.getMe.user as IUser,
        });
      },
    }
  );

  const { isLoading, mutate: loginUser } = useLoginUserMutation<Error>(
    graphqlRequestClient,
    {
      onSuccess(data: LoginUserMutation) {
        query.refetch();
        navigate(from);
      },
      onError(error: any) {
        error.response.errors.forEach((err: any) => {
          toast(err.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({ input: 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 & GraphQL Request: Authentication Guard

On the GraphQL backend, we returned a logged_in cookie that is not HTTPOnly and can be accessed by the React application.

Also, the logged_in cookie has the same expiration time as the access token cookie.


yarn add react-cookie
# or 
npm install react-cookie

Let’s create an authentication guard to protect the private routes on the React app.

The requireUser authentication guard will make a GraphQL request to retrieve the logged-in user’s credentials.

The GetMeQuery request will only be successful only if the user is logged in and when the request fails we immediately redirect the user to the login page.

src/components/requireUser.tsx


import { useCookies } from 'react-cookie';
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import graphqlRequestClient from '../clients/graphqlRequestClient';
import { useStateContext } from '../context';
import { IUser } from '../context/types';
import { GetMeQuery, useGetMeQuery } from '../generated/graphql';
import { REFRESH_ACCESS_TOKEN } from '../middleware/AuthMiddleware';
import FullScreenLoader from './FullScreenLoader';

const RequireUser = ({ allowedRoles }: { allowedRoles: string[] }) => {
  const [cookies] = useCookies(['logged_in']);
  const location = useLocation();
  const stateContext = useStateContext();

  const { isLoading, isFetching, data, refetch } = useGetMeQuery<
    GetMeQuery,
    Error
  >(
    graphqlRequestClient,
    {},
    {
      retry: 1,
      onSuccess: (data) => {
        stateContext.dispatch({
          type: 'SET_USER',
          payload: data.getMe.user as IUser,
        });
      },
      onError(error: any) {
        error.response.errors.forEach(async (err: any) => {
          if (err.message.includes('not logged in')) {
            try {
              await graphqlRequestClient.request(REFRESH_ACCESS_TOKEN);
              refetch();
            } catch (error) {
              document.location.href = '/login';
            }
          }
        });
      },
    }
  );

  const user = data?.getMe.user;

  const loading = isFetching || isLoading;

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

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

export default RequireUser;

React Query & GraphQL Request: Middleware Guard

src/middleware/AuthMiddleware.tsx


import { useCookies } from 'react-cookie';
import { useStateContext } from '../context';
import FullScreenLoader from '../components/FullScreenLoader';
import React from 'react';
import { GetMeQuery, useGetMeQuery } from '../generated/graphql';
import graphqlRequestClient from '../clients/graphqlRequestClient';
import { IUser } from '../context/types';
import { gql } from 'graphql-request';

export const REFRESH_ACCESS_TOKEN = gql`
  query {
    refreshAccessToken {
      status
      access_token
    }
  }
`;

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

const AuthMiddleware: React.FC<AuthMiddlewareProps> = ({ children }) => {
  const [cookies] = useCookies(['logged_in']);
  const stateContext = useStateContext();

  const query = useGetMeQuery<GetMeQuery, Error>(
    graphqlRequestClient,
    {},
    {
      retry: 1,
      enabled: Boolean(cookies.logged_in),
      onSuccess: (data) => {
        stateContext.dispatch({
          type: 'SET_USER',
          payload: data.getMe.user as IUser,
        });
      },
      onError(error: any) {
        error.response.errors.forEach(async (err: any) => {
          if (err.message.includes('not logged in')) {
            try {
              await graphqlRequestClient.request(REFRESH_ACCESS_TOKEN);
              query.refetch();
            } catch (error) {
              document.location.href = '/login';
            }
          }
        });
      },
    }
  );

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

  return children;
};

export default AuthMiddleware;

Create Home and Profile Pages with TailwindCSS

Home Page

src/pages/home.page.tsx


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

src/pages/profile.page.tsx


import { useStateContext } from '../context';

const ProfilePage = () => {
  const stateContext = useStateContext();

  const user = stateContext.state.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


yarn add react-router-dom
# or npm install react-router-dom

With React Router Dom v6, we can now define our routes using the object-based routing syntax. we just need to define an array of route objects and provide the routes to the individual pages we created above.

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', 'admin']} />,
      children: [
        {
          path: '',
          element: <ProfilePage />,
        },
      ],
    },
  ],
};

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

export default routes;

Update the App and Index Files

In ./src/App.tsx file, we import the routes array we defined above and provide it to the useRoutes hook provided by React Router Dom.

src/App.tsx


import { useRoutes } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import routes from './router';

function App() {
  const content = useRoutes(routes);
  return (
    <>
      <ToastContainer />
      {content}
    </>
  );
}

export default App;

Finally, wrap the StateContextProvider and the AuthMiddleware around the root app.

src/index.tsx


import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { StateContextProvider } from './context';
import { BrowserRouter as Router } from 'react-router-dom';
import AuthMiddleware from './middleware/AuthMiddleware';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 10 * 1000,
    },
  },
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <Router>
        <StateContextProvider>
          <AuthMiddleware>
            <App />
          </AuthMiddleware>
        </StateContextProvider>
        <ReactQueryDevtools initialIsOpen={false} />
      </Router>
    </QueryClientProvider>
  </React.StrictMode>
);

Conclusion

With this React Query, GraphQL CodeGen, tailwindCss, graphql-request , and React-Hook-Form example in Typescript, you’ve learned how to add access and refresh token functionality to your React.js projects.

React Query & GraphQL Request Source Code

Check out the complete source code for: