RTK Query + React Hook Form

Asynchronus form handling made easy.

Not every web app is born equal. Some of them are just static pages with some content that looks nice and presents users with necessary data, but a lot of them are more complex projects. Web pages nowadays are very often Single Page Apps which means it's at some point developers have to use some generic tool to make requests receive data and handle errors. Another thing that's almost always necessary in SPA is state management. Currently most commonly used state manager for React is Redux, since it is mature and reliable. But even Redux has some downsides, the biggest of which is having to write lots of boilerplate code. Another problem of more complex applications is form management. Some projects have a lot of them and they can become quite a pain. Again developers often will start to look for a solution to make form management easy.

In my case, the solution to all those problems was to use Redux Toolkit with built-in RTKQuery to handle the asynchronous API calls and state management (also reducing boilerplate you need to write while using Redux), along with React Hook Form library which made using forms simple and easy.

In this article, I will show you how to make both of them work together to make a seamless solution for form-heavy single-page apps.

Solution

First, install react toolkit.

npm install @reduxjs/toolkit react-redux

The next step is to configure the store by creating store.js file

./store/store.js
import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {},
})

After adding a store we need to wrap our Application inside of the store provider component inside of index.js file

index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import App from "./App";
import store from "./store/store";
import "./styles.css";

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Now react toolkit is configured to work with our application however the store is still empty which will provide us with an error.

Store.js points to redux slices (Check RTK docs for details) or Api files.

To use RTK let's declare a simple API file and configure a basic get query to fetch some data from a public API. In this case, I will use https://jsonplaceholder.typicode.com/

baseUrl property is the URL that each request will start with. Rest of the URL is passed as a parameter to each query.

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
const BASE_URL = "https://jsonplaceholder.typicode.com/";

export const exampleApi = createApi({
  reducerPath: "exampleApi",
  baseQuery: fetchBaseQuery({ baseUrl: BASE_URL }),
  endpoints: (builder) => ({
    getExampleRequest: builder.query({
      query: () => `todos/1`
    }),
    })
  })
});

export const {
  useGetExampleRequestQuery,
} = exampleApi;

Adding the prefix "use" and suffix "Query" to our endpoint name will automatically create a hook that we can use to fetch data inside our react components.

After configuring a simple query and creating a hook we needt to add our Api file to redux store.

./store/store.js
import { configureStore } from "@reduxjs/toolkit";
import { exampleApi } from "./api";
export const store = configureStore({
  reducer: {
    [exampleApi.reducerPath]: exampleApi.reducer
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(exampleApi.middleware)
});
export default store;

After that, we can use it inside our react component to fetch the data and display it to the user.

RTKQuery hooks have a lot of properties that make our lives easier f.e. isLoading or isSuccess. We can use them to conditionally render components based on the request state. Here we will display data only if our request is not loading.

import React from "react";
import "./styles.css";
import { useGetExampleRequestQuery } from "./store/api";

const App = () => {
  const { data, isLoading } = useGetExampleRequestQuery();
  return (
    <div className="App">
      <br />
      {!isLoading ? JSON.stringify(data) : "Loading"}
    </div>
  );
};
export default App;

Ok. we fetched our first request and we can display the results to the user.

Instead of just displaying this data, it would be nice to automatically fill this data into a form for the user to edit.

Now it's time for react-hook-form to kick in.

First, install the package from npm

npm install react-hook-form

Then create a react component. React Hook Form similar to RTKQuery also uses a hook to get the job done. It also has a lot of properties to make our lives easier. We need to wrap our inputs inside of <form> tag and pass the register property with a field name for each input. The handleSubmit will fire a function after hitting submit button and pass data from all registered fields to this function. To see how it works, just use console.log() and see how form data is passed.

./components/Form.jsx
import React from "react";
import { useForm } from "react-hook-form";

export const Form = () => {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data) => console.log(data);
  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register("userId")} />
        <input {...register("title")} />
        <select {...register("completed")}>
          <option value="true">True</option>
          <option value="false">False</option>
        </select>
        <input type="submit" />
      </form>
    </>
  );
};
export default Form;

Now it's time to fill our form with data received from the endpoint we created earlier. To achieve this just pass the data from useGetExampleRequestQuery as a prop into our Form component. We can also name our data with an alias f.e. data: receivedData.This comes in handy if you have multiple query hooks inside one component.

import React from "react";
import "./styles.css";
import { useGetExampleRequestQuery } from "./store/api";
import Form from "./components/Form";
const App = () => {
  const { data: receivedData, isLoading } = useGetExampleRequestQuery();
  return (
    <div className="App">
      <br />
      {!isLoading ? <Form defaultData={receivedData} /> : "Loading"}
    </div>
  );
};
export default App;

Now simply pass the defaultData into {defaultValues} property of useForm hook. If data received from the endpoint has the same field names as registered input names, data will automatically fill out our form.

./components/Form.jsx
import React from "react";
import { useForm } from "react-hook-form";

export const Form = ({ defaultData }) => {
  const { register, handleSubmit } = useForm({ defaultValues: defaultData });
  const onSubmit = (data) => console.log(data);
  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register("userId")} />
        <input {...register("title")} />
        <select {...register("completed")}>
          <option value="true">True</option>
          <option value="false">False</option>
        </select>
        <input type="submit" />
      </form>
    </>
  );
};
export default Form;

But reading the data and filling out the form is only half of the problem. The most important thing in forms is the ability to send data to the server. For this we use RTKQuery. Since we will modify data with our POST request we have to configure a mutation instead of query in api.js file

./store/api.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
const BASE_URL = "https://jsonplaceholder.typicode.com/";

export const exampleApi = createApi({
  reducerPath: "exampleApi",

  baseQuery: fetchBaseQuery({ baseUrl: BASE_URL }),

  endpoints: (builder) => ({
    getExampleRequest: builder.query({
      query: () => `todos/1`
    }),

    postExampleRequest: builder.mutation({
      query: (body) => ({
        url: "todos",
        method: "POST",
        body
      })
    })
  })
});

export const {
  useGetExampleRequestQuery,
  usePostExampleRequestMutation
} = exampleApi;

By wrapping our request in use***Mutation we will create a slightly different hook that will allow us to trigger a function and pass body into our request when the form is submitted. When using a mutation hook we need to declare an array in which the first element will be the name of our trigger function, a second element is an object contacting properties like data, isLoading, isSuccess etc. similar to query hook.

Finally to send data just use the trigger function from the mutation hook inside the submit function and pass the form data which will now serve as a request body.

import React from "react";
import { useForm } from "react-hook-form";
import { usePostExampleRequestMutation } from "../store/api";
export const Form = () => {
  const { register, handleSubmit } = useForm();

  const [sendRequest, { data, isSuccess }] = usePostExampleRequestMutation();

  const onSubmit = (data) => sendRequest(data);

  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register("userId")} />
        <input {...register("title")} />
        <select {...register("completed")}>
          <option value="true">True</option>
          <option value="false">False</option>
        </select>
        <input type="submit" />
      </form>
    </>
  );
};
export default Form;

When the request succeeds we can show received data to the user.

import React from "react";
import { useForm } from "react-hook-form";
import { usePostExampleRequestMutation } from "../store/api";
export const Form = () => {
  const { register, handleSubmit } = useForm();

  const [sendRequest, { data, isSuccess }] = usePostExampleRequestMutation();
  const onSubmit = (data) => sendRequest(data);
  return (
    <>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register("userId")} />
        <input {...register("title")} />
        <select {...register("completed")}>
          <option value="true">True</option>
          <option value="false">False</option>
        </select>
        <input type="submit" />
      </form>
      {isSuccess && (
        <div>
          <h3>Resopnse Data: </h3>
          <div> userId: {data?.userId} </div>
          <div> title: {data?.title} </div>
          <div> completed: {data?.completed} </div>
          <div> id: {data?.id} </div>
        </div>
      )}
    </>
  );
};
export default Form;

Try it out on code sandbox:

RTK and React Hook Form have many features that will make your life a lot easier and spend a lot less time managing your state requests and forms.

To get a better idea of how they work it's best to visit the documentation pages:

https://redux-toolkit.js.org/introduction/getting-started

https://react-hook-form.com/

Cheers.

Did you find this article valuable?

Support Marek Król by becoming a sponsor. Any amount is appreciated!