How to use Tanstack React Query with React Hook form and ZOD

Yassine NassibYassine Nassib
6 min read

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.

0
Subscribe to my newsletter

Read articles from Yassine Nassib directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yassine Nassib
Yassine Nassib