React Router Forms and Actions: Handling Submission and Validation with Ease

franciscomendes10866

Francisco Mendes

Posted on January 1, 2023

React Router Forms and Actions: Handling Submission and Validation with Ease

Introduction

In the past we used loaders 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:

yarn create vite router-actions --template react
cd router-actions
Enter fullscreen mode Exit fullscreen mode

Now we can install the necessary dependencies:

yarn add react-router-dom superstruct
Enter fullscreen mode Exit fullscreen mode

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.

// @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;
Enter fullscreen mode Exit fullscreen mode

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.

// @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;
Enter fullscreen mode Exit fullscreen mode

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:

// @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 }) => {
  // ...
};

// ...
Enter fullscreen mode Exit fullscreen mode

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 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.

// @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("/");
};

// ...
Enter fullscreen mode Exit fullscreen mode

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.

// @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;
Enter fullscreen mode Exit fullscreen mode

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.

// @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} />;
};
Enter fullscreen mode Exit fullscreen mode

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 the link to the github repository.

💖 💪 🙅 🚩
franciscomendes10866
Francisco Mendes

Posted on January 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related