# Building Forms in React: How to Handle State, Validation, API Calls, and Toasts

Hi there, today I am sharing my knowledge of form building in React.

Very often, we have found ourselves writing a bunch of `useState` or even worse, using a global state management library like Redux to manage form values, errors, loading states, etc. We now have tools that make form management in React a very easy task.

This blog is not just a formik / react-hook-form tutorial, I want to cover everything from writing jsx to validating inputs, to handling form submissions, and finally toast the user notifying them of their success or failure. Let's begin :)

# What are we building?

Just a single page that lists all the posts from the JSONPlaceholder API, with a form to add a new post. On submitting the form, the API returns the created post which we will append to the already fetched list of posts.

![What are we building?](https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExYnJndHFia2U5YjQzaXhrcW15aDZxeG9ncHY3MHpxNjJ4cHYzdGFoNyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/Vbr8OH4ucWmtFFO4X7/giphy.gif align="center")

# Choosing libraries

Let's not re-invent the wheel for our task. Assuming you already know how state works in React, let's not use `useState` to store our form data. Same goes for validation, let's not write our own validation logic in our form's `onSubmit` handler. I will use [react-hook-form](https://react-hook-form.com/) for form state and [zod](https://zod.dev/) for validation. You can choose [formik](https://formik.org/) or [yup](https://www.npmjs.com/package/yup) as well.

For API calls, I will use [axios](https://axios-http.com/docs/intro) and [@tanstack/react-query](https://tanstack.com/query/latest). [JSONPlaceholder](https://jsonplaceholder.typicode.com/) as the backend :)

I will use [sonner](https://sonner.emilkowal.ski/) as my toast component since it's easy to use and looks good stock.

(Optional) I will also add [Material UI](https://mui.com/material-ui/) because it has pre-built components with decent styles.

# Project Setup

1. I created a Vite + React + TS app, you can stick to JS that's just preference.
    
2. Installed all the required dependencies.
    

%[https://github.com/ravindranag/hashnode-building-forms-in-react] 

# Config

We will configure axios and react-query first:

```typescript
// src/config/axios.ts
import axios from "axios";

const api = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com'
})

export default api


// src/config/queryClient.ts
import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient()

export default queryClient
```

Then we wrap our app with `QueryClientProvider`

```typescript
// src/main.ts

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
    </QueryClientProvider>
  </StrictMode>,
)
```

# Queries and Mutations

`@tanstack/react-query` 's `useQuery` and `useMutation` hooks require a `queryFn` and a `mutationFn` respectively. Let's get them ready:

```typescript
// src/utils/query/posts.ts
import api from "@/config/axios";
import { PostDto } from "@/dtos/posts";

export const getAllPosts = () => api.get<PostDto[]>('/posts')
getAllPosts.getQueryKey = () => ['posts', 'all']
```

I like to define a `getQueryKey` function for the query functions, makes it easier to work with `useQuery`.

```typescript
// src/utils/mutations/posts.ts
import api from "@/config/axios";
import { CreatePostDto, PostDto } from "@/dtos/posts";

export const createNewPost = (body: CreatePostDto) =>
  api.post<PostDto>("/posts", body);
```

# The Home Page

```typescript
const PostListCreate = () => {
  const { data } = useQuery({
    queryFn: getAllPosts,
    queryKey: getAllPosts.getQueryKey()
  })

  return (
    <PageContainer padding={4} gap={2}>
      <CreatePostForm />
      <Typography variant="overline" color='grey.900'>Latest Posts</Typography>
      <Divider />
      {data && <PostGrid posts={data.data} />}
    </PageContainer>
  )
}
```

Very simple page, nothing serious here

# The Form Component

First, let's define the form validation schema with zod:

```typescript
const validationSchema = z.object({
  title: z.string().min(1, "Title is required"),
  body: z.string().min(1, "Body is required"),
});

type FormData = z.infer<typeof validationSchema>;
```

Second, write JSX:

```typescript
const CreatePostForm = () => {
  const { register, handleSubmit, formState, reset } = useForm<FormData>({
    resolver: zodResolver(validationSchema),
  });
  const { mutateAsync } = useMutation({
    mutationFn: createNewPost,
  });

  const onSubmit = (data: FormData) => {
    ...
  };

  return (
    <Stack component="form" gap={2} onSubmit={handleSubmit(onSubmit)}>
      <TextField
        {...register("title")}
        label="Title"
        error={Boolean(formState.errors.title)}
        helperText={formState.errors.title?.message}
      />
      <TextField
        {...register("body")}
        label="Body"
        error={Boolean(formState.errors.body)}
        helperText={formState.errors.body?.message}
        multiline
        minRows={5}
      />
      <Button variant="contained" type="submit" sx={{ alignSelf: "end" }}>
        Post
      </Button>
    </Stack>
  );
};
```

Great! now we have a form to look at. Let's finish the `onSubmit` function:

```typescript
const onSubmit = (data: FormData) => {
    const payload = {
      ...data,
      userId: "1",
    };

    toast.promise(
      mutateAsync(payload).then((res) => {
        // reset the form to accept new submissions
        reset();

        // After a successful response, you can just refresh the getAllPosts query to update the list
        // But this does not work in case of JSONPlaceholder
        // queryClient.invalidateQueries({
        //   queryKey: getAllPosts.getQueryKey()
        // })

        // Adding the newly created post to the post list manually to simulate a refresh
        const prevData: AxiosResponse<PostDto[]> = queryClient.getQueryData(
          getAllPosts.getQueryKey(),
        )!;
        const newData = {
          ...prevData,
          data: [res.data, ...prevData.data],
        };
        queryClient.setQueryData(getAllPosts.getQueryKey(), newData);
        return res;
      }),
      {
        loading: "Creating your post...",
        success: "Post created",
        error: (err) => {
          console.error("error: ", err);
          return err.message;
        },
      },
    );
  };
```

Here I am using `sonner`'s `toast.promise` function that dynamically changes the toast based on the promise's status.

And that's it :) We have a very nice form that shows validation, makes api request, and renders a toast. All of that without any `useState`.

[GitHub repo](https://github.com/ravindranag/hashnode-building-forms-in-react) for this project

Thank you for reading! I am very excited to publish my first blog on Hashnode! Please let me know your thoughts in the comments 😋
