Forms
In this step, we'll be learning about the Refine's useForm
hook to manage forms for creating and updating records.
Refine's useForm
has extended versions with more features and compatibility with other libraries. To learn more about the useForm
hook, please refer to the Forms guide.
useForm
hook can be used for 3 different actions;
create
: To create a new record for a resource using the data provider'screate
method.edit
: To update an existing record for a resource using the data provider'supdate
method.clone
: To create a new record using an existing record's data as a template using the data provider'screate
method.
In this step, we'll be covering the create
and edit
actions. Check out the Clone section of the Forms guide for information about the clone
action.
Implementing the create
Method
To create a record using Refine's useForm
and useCreate
hooks, first we need to implement the create
method in our data provider. This method will be called when we use the useForm
with create
action.
The create
method will receive resource
, variables
and meta
properties. resource
will be the name of the entity we're creating. variables
will be an object containing the data we're sending to the API. meta
will be an object containing any additional data we're passing to the hook.
products
entity of our fake API expects us to create a record using the /products
endpoint with a POST
request. So, we'll be using the resource
and variables
properties to make our request.
Update your src/providers/data-provider.ts
file by adding the following lines:
import type { DataProvider } from "@refinedev/core";
const API_URL = "https://api.fake-rest.refine.dev";
export const dataProvider: DataProvider = {
create: async ({ resource, variables }) => {
const response = await fetch(`${API_URL}/${resource}`, {
method: "POST",
body: JSON.stringify(variables),
headers: {
"Content-Type": "application/json",
},
});
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return { data };
},
update: async ({ resource, id, variables }) => {
/* ... */
},
getList: async ({ resource, pagination, filters, sorters }) => {
/* ... */
},
getOne: async ({ resource, id }) => {
/* ... */
},
/* ... */
};
Using the useForm
Hook
After implementing the create
method, we'll be able to call useForm
hook and create a single record from our API. Let's create a component called CreateProduct
and mount it inside our <Refine />
component. Then, we'll use the useForm
hook inside our CreateProduct
to create a record of products
entity from our API.
Now, we'll mount our CreateProduct
component inside our <Refine />
component. Update your src/App.tsx
file by adding the following lines::
import { Refine } from "@refinedev/core";
import { dataProvider } from "./providers/data-provider";
import { ShowProduct } from "./pages/products/show";
import { EditProduct } from "./pages/products/edit";
import { ListProducts } from "./pages/products/list";
import { CreateProduct } from "./pages/products/create";
export default function App(): JSX.Element {
return (
<Refine dataProvider={dataProvider}>
{/* <ShowProduct /> */}
{/* <EditProduct /> */}
{/* <ListProducts /> */}
<CreateProduct />
</Refine>
);
}
We'll be using the useForm
hook and have form fields for name
, description
, price
, material
and category
.
Update your src/pages/products/create.tsx
file by adding the following lines:
import { useForm } from "@refinedev/core";
export const CreateProduct = () => {
const { onFinish, mutation } = useForm({
action: "create",
resource: "products",
});
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Using FormData to get the form values and convert it to an object.
const data = Object.fromEntries(new FormData(event.target).entries());
// Calling onFinish to submit with the data we've collected from the form.
onFinish(data);
};
return (
<form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" />
<label htmlFor="description">Description</label>
<textarea id="description" name="description" />
<label htmlFor="price">Price</label>
<input type="number" id="price" name="price" step=".01" />
<label htmlFor="material">Material</label>
<input type="text" id="material" name="material" />
<label htmlFor="category">Category ID</label>
<input type="number" id="category" name="category" />
{mutation.isSuccess && <span>successfully submitted!</span>}
<button type="submit">Submit</button>
</form>
);
};
Now, we'll be able to create a record using our CreateProduct
component.
Updating Values Before Submitting
Although we're able to create a record using our CreateProduct
component, we're not quite finished yet. We need to make sure that the values we're sending to the API are in the correct format.
Our fake API's products
field requires us to send the price
as a string with 2 decimal points. So, we need to make sure that the price
value is in correct format before submitting the form. Also our category
field requires us to send an object with the id
property. So, we need to make sure that the category
value is in correct format before submitting the form.
Update your src/pages/products/create.tsx
file by adding the following lines::
import { useForm } from "@refinedev/core";
export const CreateProduct = () => {
const { onFinish, mutation } = useForm({
action: "create",
resource: "products",
});
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Using FormData to get the form values and convert it to an object.
const data = Object.fromEntries(new FormData(event.target).entries());
// Calling onFinish to submit with the data we've collected from the form.
onFinish({
...data,
price: Number(data.price).toFixed(2),
category: { id: Number(data.category) },
});
};
return <form onSubmit={onSubmit}>{/* ... */}</form>;
};
We're now able to create a record with the price
value properly formatted.
Modifying the values before submission is supported in all the derivatives of the useForm
hook but the implementation may differ slightly. To learn more about their usage, please refer to the Modifying Data Before Submission section of the Forms guide.
Handling Relationships
Notice that we've added a category
field to our CreateProduct
component. This field will be used to select the category of the product we're creating.
Our fake API has the categories
entity which we use in our products
entity as a relationship. So, we need to make sure that we're sending the correct data to the API.
To handle this relation in our forms, Refine offers a useSelect
hook. This hook will be used to fetch the data for the relationship and provide us options for the <select>
element.
Update your src/pages/products/create.tsx
file by adding the following lines to use useSelect
and the <select>
element:
import { useForm, useSelect } from "@refinedev/core";
export const CreateProduct = () => {
const { onFinish, mutation } = useForm({
action: "create",
resource: "products",
});
const { options } = useSelect({
resource: "categories",
// optionLabel: "title", // Default value is "title" so we don't need to provide it.
// optionValue: "id", // Default value is "id" so we don't need to provide it.
});
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
/* ... */
};
return (
<form onSubmit={onSubmit}>
{/* ... */}
<label htmlFor="category">Category</label>
<select id="category" name="category">
{options?.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{/* ... */}
</form>
);
};
And we'll finally be able to create a proper record with all the necessary fields in right format and with the correct relationships.
Refine allows you to use different types of relationships in your forms, tables or in other ways to display data. To learn more about the relationships, please refer to the Relationships section of the Data Fetching guide.
Refactoring src/pages/products/edit.tsx
with useForm
Now we've learned how to use the useForm
hook to create a record. Let's refactor our EditProduct
component to use the useForm
hook to update a record.
Let's start with mounting our EditProduct
component inside our <Refine />
component. Update your src/App.tsx
file by adding the following lines:
import { Refine } from "@refinedev/core";
import { dataProvider } from "./providers/data-provider";
import { ShowProduct } from "./pages/products/show";
import { EditProduct } from "./pages/products/edit";
import { ListProducts } from "./pages/products/list";
import { CreateProduct } from "./pages/products/create";
export default function App(): JSX.Element {
return (
<Refine dataProvider={dataProvider}>
{/* <ShowProduct /> */}
<EditProduct />
{/* <ListProducts /> */}
{/* <CreateProduct /> */}
</Refine>
);
}
Now, we should be seeing our EditProduct
component, let's import our useForm
hook to replace both useOne
and useUpdate
hooks.
We'll also be re-using the elements we've used for the fields in our <CreateProduct />
component.
useForm
hook requires us to pass the id
property when we're using the edit
action.
useForm
will fetch the record first to make sure we're able to provide default values for the fields.
In the extensions of the useForm
hook, this is handled automatically. To learn more about the extensions, please refer to the Forms guide.
Update your src/pages/products/edit.tsx
file by adding the following lines:
import { useForm, useSelect } from "@refinedev/core";
export const EditProduct = () => {
const { onFinish, mutation, query } = useForm({
action: "edit",
resource: "products",
id: "123",
});
const record = query.data?.data;
const { options } = useSelect({
resource: "categories",
});
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Using FormData to get the form values and convert it to an object.
const data = Object.fromEntries(new FormData(event.target).entries());
// Calling onFinish to submit with the data we've collected from the form.
onFinish({
...data,
price: Number(data.price).toFixed(2),
category: { id: Number(data.category) },
});
};
return (
<form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" defaultValue={record?.name} />
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
defaultValue={record?.description}
/>
<label htmlFor="price">Price</label>
<input
type="text"
id="price"
name="price"
pattern="\d*\.?\d*"
defaultValue={record?.price}
/>
<label htmlFor="material">Material</label>
<input
type="text"
id="material"
name="material"
defaultValue={record?.material}
/>
<label htmlFor="category">Category</label>
<select id="category" name="category">
{options?.map((option) => (
<option
key={option.value}
value={option.value}
selected={record?.category.id == option.value}
>
{option.label}
</option>
))}
</select>
{mutation.isSuccess && <span>successfully submitted!</span>}
<button type="submit">Submit</button>
</form>
);
};
Now we'll be able to update a record using our EditProduct
component with useForm
and provide all the necessary fields with the correct formats and relationships.
Summary
Now we've learned how to use the useForm
hook to create and update records. We've also learned how to handle relationships in our forms.
Refine's useForm
hooks are not limited to these abilities. useForm
hook will also handle;
- Invalidation of the related queries,
- Redirecting to a different page after submission,
- Server side validation,
- Optimistic updates for the related queries,
- Auto saving,
- Notifications and more...
To learn more about the useForm
hook, please refer to the Forms guide.
In the next step, we'll be learning about the Refine's useTable
hook and how to display a list of records in a table.
import { useList } from "@refinedev/core"; export const ListProducts = () => { const { data, isLoading } = useList({ resource: "products", pagination: { current: 1, pageSize: 10 }, sorters: [{ field: "name", order: "asc" }], filters: [{ field: "material", operator: "eq", value: "Aluminum" }], }); if (isLoading) { return <div>Loading...</div>; } return ( <div> <h1>Products</h1> <ul> {data?.data?.map((product) => ( <li key={product.id}> <p> {product.name} <br /> Price: {product.price} <br /> Material: {product.material} </p> </li> ))} </ul> </div> ); };