React Router Forms and Actions: Handling Submission and Validation with Ease
Tags: react, webdev, javascript, tutorial
Published: 2023-01-01 15:13:58

## Introduction

In the past we used [loaders](https://dev.to/franciscomendes10866/react-router-data-fetching-2ikh) to read data from an external source, but we can also do more things at the route level, such as mutating the data, such as submitting a form.

In today's article we are going to build an app that has two routes. The main route is where we are going to do the readings and listing that same result in a list, as well as creating another page that will contain just one form where we are going to submit new data.

After submitting the data, the idea is to validate them before sending them to an api or saving them locally.

## Assumed knowledge

The following would be helpful to have:

- Basic knowledge of React
- Basic knowledge of React Router
- Basic knowledge of JSON schema validation

## Getting Started

### Project Setup

Run the following command in a terminal:

```bash
yarn create vite router-actions --template react
cd router-actions
```

Now we can install the necessary dependencies:

```bash
yarn add react-router-dom superstruct
```

### Build the Components

The first step is to create the component responsible for the Layout of our application, where the `<Outlet />` component will be used.

```js
// @src/components/Layout.jsx
import { Link, Outlet } from "react-router-dom";

const Layout = () => {
  return (
    <div>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/post">Create New Post</Link>
          </li>
        </ul>
      </nav>

      <Outlet />
    </div>
  );
};

export default Layout;
```

With the only component we need created, we can now move on to the next step.

### Build the Pages

The first page to be created will be `Home.jsx`, which will hold a loader that will make an http request to an Api, returning the result. Later we will consume this data in the component using the `useLoaderData()` hook.

```js
// @src/pages/Home.jsx
import { useLoaderData } from "react-router-dom";

export const loader = async () => {
  let data;
  try {
    const res = await fetch("/mocked-api/post");
    data = await res.json();
  } catch {
    data = null;
  }
  return data;
};

const Home = () => {
  const list = useLoaderData();

  return (
    <div>
      <h1>Home page</h1>

      {list?.map((item, itemIndex) => (
        <ul key={itemIndex}>
          <li>{item?.title}</li>
        </ul>
      ))}
    </div>
  );
};

export default Home;
```

Now let's go to the most important page of today's article. Let's create the `CreatePost.jsx` page where we'll first create the action and define the schema of the form, as well as import what will be needed:

```js
// @src/pages/CreatePost.jsx
import { Form, redirect, json, useActionData } from "react-router-dom";
import { assert, object, string, nonempty, StructError } from "superstruct";

const articleSchema = object({
  title: nonempty(string()),
  content: nonempty(string()),
});

export const action = async ({ request }) => {
  // ...
};

// ...
```

Inside the action, the first thing we're going to do is get the `formData` using the `request` object. Then we will create an object from the `formData` data that will later be validated by our [superstruct](https://github.com/ianstormtaylor/superstruct) schema.

If an error occurs, we have to check if it is a validation error and if so, we will return an object containing only the keys and error messages according to the form fields in json format.

If there is no error during validation, we submit the data to the API and redirect it to the main page.

```js
// @src/pages/CreatePost.jsx
import { Form, redirect, json, useActionData } from "react-router-dom";
import { assert, object, string, nonempty, StructError } from "superstruct";

const articleSchema = object({
  title: nonempty(string()),
  content: nonempty(string()),
});

export const action = async ({ request }) => {
  const form = await request.formData();

  const formToJSON = {};
  for (const [key, value] of [...form.entries()]) {
    formToJSON[key] = value;
  }

  try {
    assert(formToJSON, articleSchema);
  } catch (err) {
    if (err instanceof StructError) {
      const fieldsErrors = err.failures().reduce(
        (acc, { key, message }) => ({
          ...acc,
          [key]: message,
        }),
        {}
      );
      return json(fieldsErrors);
    }
    console.error(err);
  }

  try {
    await fetch("/mocked-api/post", {
      method: "POST",
      body: JSON.stringify(formToJSON),
    });
  } catch (err) {
    console.error(`[ACTION ERROR]: ${err}`);
  }

  return redirect("/");
};

// ...
```

Still on this page, we'll use the `useActionData()` hook to consume the json if an error occurs related to any of the form's fields.

```js
// @src/pages/CreatePost.jsx
import { Form, redirect, json, useActionData } from "react-router-dom";
import { assert, object, string, nonempty, StructError } from "superstruct";

// ...

const CreatePost = () => {
  const actionData = useActionData();

  return (
    <section>
      <h2>Create New Post</h2>

      <Form method="post">
        <input name="title" placeholder="Post title" />
        {actionData?.title && <small>{actionData?.title}</small>}

        <br />
        <textarea name="content" placeholder="Post content" />
        {actionData?.content && <small>{actionData?.content}</small>}

        <br />
        <button type="submit">Submit</button>
      </Form>
    </section>
  );
};

export default CreatePost;
```

### Router Setup

Last but not least, we have to register the application routes and assign the loader on the `Home.jsx` page and assign the action on the `CreatePost.jsx` page.

```js
// @src/App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider,
} from "react-router-dom";

import Layout from "./components/Layout";
import HomePage, { loader } from "./pages/Home";
import CreatePostPage, { action } from "./pages/CreatePost";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route element={<Layout />}>
      <Route index element={<HomePage />} loader={loader} />
      <Route path="/post" element={<CreatePostPage />} action={action} />
    </Route>
  )
);

export const App = () => {
  return <RouterProvider router={router} />;
};
```

## Conclusion

As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.

If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave [here](https://github.com/FranciscoMendes10866/router-actions-msw) the link to the github repository.