Hi there , if you are reading this article , it means that you want to learn how to use Tanstack React Query with React Hook form and ZOD for data validation , you are in the right place.
What you will learn :
How to create ZOD schema for form validation
How to handle Forms using React hook Form
How to create mutation with Tanstak React Query
Let's go!
first of all , let's take a look at the folders/files architecture :
Source code on Github :
https://github.com/chikno/whitebeard-dev/tree/React-Query-with-React-Hook-form-and-ZOD
Install the dependencies
let's begin by installing some dependencies.
I have a huge preference for pnpm , but you can use whatever Package manager you are comfortable with.
pnpm add axios @tanstack/react-query react-hook-form @hookform/resolvers zod
Create the API provider
create a file api-provider.tsx in your app and fill it with this content;
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as React from 'react';
export const queryClient = new QueryClient();
export function APIProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
the next step is to wrap our app component with the query client provider.
Open index.tsx file and update it like this
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { APIProvider } from './common/api-provider';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<APIProvider>
<App />
</APIProvider>
</React.StrictMode>
);
Create the Client
Let's create our axios client once . then we will use it to send every request to the API. it will prevents us to write each time the request options.
create a file called : client.js , then put this code on it
import axios from 'axios';
export const client = axios.create({
baseURL: "YOUR_API_BASE_URL"
});
Create the form page
Let's Create the formpage, on this example ; we will have a page with 2 fields , username and password.
import React from 'react';
import './App.css';
import { useForm, type SubmitHandler } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useLogin } from './common/use-login';
import { AuthStatus } from './types/types';
import { AxiosError } from 'axios';
const schema = z.object({
username: z
.string({
required_error: 'Email is required',
})
.email('Invalid email format'),
password: z
.string({
required_error: 'Password is required',
})
.min(6, 'Password must be at least 6 characters'),
});
export type FormType = z.infer<typeof schema>;
export type FormProps = {
onSubmit?: SubmitHandler<FormType>;
};
const ErrorMessage = ({ message }: { message: string | undefined }) => {
return (
<div className="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<span className="font-medium">{message}</span>
</div>
)
}
function App() {
const inputStyles = {
className: 'border-[1px] border-gray rounded-md mb-4 w-72 p-2'
}
const { handleSubmit, register, formState: { errors } } = useForm<FormType>({
resolver: zodResolver(schema),
});
return (
<div className=" flex-1 flex justify-center items-center w-72 m-auto w-1/2">
<form onSubmit={handleSubmit(onSubmit)}>
<div className='flex flex-col justify-between'>
<input placeholder='Username' {...inputStyles} defaultValue="" {...register("username")} />
{errors.username && <ErrorMessage message={errors?.username?.message} />}
<input {...inputStyles} placeholder='Password' {...register("password")} />
{errors.password && <ErrorMessage message={errors?.password?.message} />}
<button type="submit" className="text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700">Submit</button>
</div>
</form>
</div>
);
}
export default App;
let's explain.
First we create a scheme using zod librarie , the scheme containes to fields , username and password , then we add some validation rules on it.
const schema = z.object({
username: z
.string({
required_error: 'Email is required',
})
.email('Invalid email format'),
password: z
.string({
required_error: 'Password is required',
})
.min(6, 'Password must be at least 6 characters'),
});
Then we define a FormProps type to be passed to the submit handler.
export type FormProps = {
onSubmit?: SubmitHandler<FormType>;
};
next step is to descrtructure the useForm Hook to get the register , handleSubmit and form errors,
const { handleSubmit, register , formState: { errors }} = useForm<FormType>({
resolver: zodResolver(schema),
});
Mutation custom hook
Before we implement the submit mehtod , let's create the custom hook for sending the Request to the server. it will be a custom hook so it can be easy to manage and also reusable.
let's create 2 files, types.ts and use-login.ts.
Types.ts
export type AuthStatus = {
success: boolean;
err?: {
message: string;
};
token: string;
};
use-login.ts
import type { AxiosError } from 'axios';
import { createMutation, CreateMutationOptions } from 'react-query-kit';
import { client } from './client';
import type { AuthStatus } from '../types/types';
type Variables = { username: string; password: string };
type Response = AuthStatus;
export const useLogin = createMutation<Response, Variables, AxiosError>({
mutationFn: async (variables: Variables) =>
client({
url: 'api/auth/authenticate',
method: 'POST',
data: variables,
}).then((response) => response.data),
onSuccess: (data:Response) => {
console.log('Login successful', data);
},
onError: (error: AxiosError) => {
console.error('Login failed', error);
},
} as CreateMutationOptions<Response, Variables, AxiosError>);
Submit Method implementation
Let's update our form page bu adding the submit method and calling the use-login custom hook.
const { mutate: login } = useLogin();
const onSubmit: FormProps['onSubmit'] = (data) => {
login(data, {
onSuccess: (response: AuthStatus) => {
console.log('Authenticated successfully' + response)
},
onError: (err: AxiosError) => {
console.log('Authentication error' + err)
},
});
};
What we did here is that we desctructred the mutation method from our use-login hoo, then used it in the submit action.
here is the complete form.tsx codeimport React from 'react';
import './App.css';
import { useForm, type SubmitHandler } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useLogin } from './common/use-login';
import { AuthStatus } from './types/types';
import { AxiosError } from 'axios';
const schema = z.object({
username: z
.string({
required_error: 'Email is required',
})
.email('Invalid email format'),
password: z
.string({
required_error: 'Password is required',
})
.min(6, 'Password must be at least 6 characters'),
});
export type FormType = z.infer<typeof schema>;
export type FormProps = {
onSubmit?: SubmitHandler<FormType>;
};
const ErrorMessage = ({ message }: { message: string | undefined }) => {
return (
<div className="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<span className="font-medium">{message}</span>
</div>
)
}
function App() {
const inputStyles = {
className: 'border-[1px] border-gray rounded-md mb-4 w-72 p-2'
}
const { handleSubmit, register, formState: { errors } } = useForm<FormType>({
resolver: zodResolver(schema),
});
const { mutate: login } = useLogin();
const onSubmit: FormProps['onSubmit'] = (data) => {
login(data, {
onSuccess: (response: AuthStatus) => {
},
onError: (err: AxiosError) => {
},
});
};
return (
<div className=" flex-1 flex justify-center items-center w-72 m-auto w-1/2">
<form onSubmit={handleSubmit(onSubmit)}>
<div className='flex flex-col justify-between'>
<input placeholder='Username' {...inputStyles} defaultValue="" {...register("username")} />
{errors.username && <ErrorMessage message={errors?.username?.message} />}
<input {...inputStyles} placeholder='Password' {...register("password")} />
{errors.password && <ErrorMessage message={errors?.password?.message} />}
<button type="submit" className="text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700">Submit</button>
</div>
</form>
</div>
);
}
export default App;
That's it.
Happy coding and if you find this article usefull or if you have a better idea to submit forms , dont forget to leave a comment so we can discuss it .
Happy coding.