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, Material UI and React Hook Form: Login and Signup Forms
- React, RTK Query, React Hook Form and Material UI – Image Upload
- React + Redux Toolkit: JWT Authentication and Authorization
- React.js + Redux Toolkit: Refresh Tokens Authentication
- Build Vue.js, Vue Query, and Axios CRUD App with RESTful API
React Query and GraphQL-CodeGen Overview
On the Homepage, the user clicks on the “SignUp” button to create an account.
The user is taken to the signup page where he is required to provide his credentials.
Next, the user is redirected to the login page where he needs to provide his credentials to log in.
After the user has been successfully authenticated by the GraphQL server, React then redirects the user to the profile page.
You can inspect the “Application” tab in the dev tools to see the cookies returned by the GraphQL server.
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'
forgraphql-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 bygraphql-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: