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
Now we can install the necessary dependencies:
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.
// @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.
// @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:
// @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 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("/");
};
// ...
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;
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} />;
};
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.
Top comments (1)
Thats a nice post. I would like to learn two more things how to handle dynamic fields with Forms component from react-router-dom and also how to use something like react-hook-form with forms and actions.