Skip to main content

useForm

refine offers a React Hook Form adapter(@pankod/refine-react-hook-form) that allows you to use the React Hook Form library with refine. Thus, you can manage your forms in headless way.

All of React Hook Form's features are supported and you can use all of the React Hook Form's examples with no changes just copy and paste them into your project.

Installation

Install the @pankod/refine-react-hook-form library.

npm i @pankod/refine-react-hook-form

Usage

In the following example, we will step-by-step create an example of a headless form with React Hook Form capabilities.

Create resource pages

We simply create a <PostList>, <PostCreate>, and <PostEdit> components and pass to the <Refine> component as a resource.

src/posts/list.tsx
export const PostList: React.FC = () => {
return <></>;
};
src/posts/create.tsx
export const PostCreate: React.FC = () => {
return <></>;
};
src/posts/edit.tsx
export const PostEdit: React.FC = () => {
return <></>;
};
src/App.tsx
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import dataProvider from "@pankod/refine-simple-rest";

import { PostList, PostCreate, PostEdit } from "pages/posts";

const App: React.FC = () => {
return (
<Refine
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
routerProvider={routerProvider}
resources={[
{
name: "posts",
list: PostList,
create: PostCreate,
edit: PostEdit,
},
]}
/>
);
};

export default App;

Let's develop the <PostList> component for directing to the <PostCreate> and the <PostEdit> component.

src/posts/list.tsx
import { useTable, useNavigation } from "@pankod/refine-core";

import { IPost } from "interfaces";

export const PostList: React.FC = () => {
const { tableQueryResult } = useTable<IPost>();
const { edit, create } = useNavigation();

return (
<div>
<button onClick={() => create("posts")}>Create Post</button>
<table>
<thead>
<td>ID</td>
<td>Title</td>
<td>Actions</td>
</thead>
<tbody>
{tableQueryResult.data?.data.map((post) => (
<tr key={post.id}>
<td>{post.id}</td>
<td>{post.title}</td>
<td>
<button onClick={() => edit("posts", post.id)}>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
List Page

Create Form

Firts, we need to import the useForm hook from the @pankod/refine-react-hook-form library. Then we create a basic example of post a create form. All we have to do is to pass the onFinish to handleSubmit.

We also use useSelect to fetch category options.

Refer to the useSelect documentation for detailed information.

src/posts/create.tsx
import { useForm } from "@pankod/refine-react-hook-form";
import { useSelect } from "@pankod/refine-core";

export const PostCreate: React.FC = () => {
const {
refineCore: { onFinish, formLoading },
register,
handleSubmit,
formState: { errors },
} = useForm();

const { options } = useSelect({
resource: "categories",
});

return (
<form onSubmit={handleSubmit(onFinish)}>
<label>Title: </label>
<input {...register("title", { required: true })} />
{errors.title && <span>This field is required</span>}
<br />
<label>Status: </label>
<select {...register("status")}>
<option value="published">published</option>
<option value="draft">draft</option>
<option value="rejected">rejected</option>
</select>
<br />
<label>Category: </label>
<select
defaultValue={""}
{...register("category.id", { required: true })}
>
<option value={""} disabled>
Please select
</option>
{options?.map((category) => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
{errors.category && <span>This field is required</span>}
<br />
<label>Content: </label>
<br />
<textarea
{...register("content", { required: true })}
rows={10}
cols={50}
/>
{errors.content && <span>This field is required</span>}
<br />
<input type="submit" value="Submit" />
{formLoading && <p>Loading</p>}
</form>
);
};
Create Form

Edit Form

Edit form is very similar to create form. @pankod/refine-react-hook-form sets the default values for the form fields according to the id of the route and fetch the data from the server. By default, it uses the id from the route. It can be changed with the setId function or id property.

However, we need to pass defaultValues to the useSelect hook to make sure that the category id from data is in the options. Otherwise, the category will not match the existing options. Since the options are async, we need to reset the relavent field every time the options are changed.

src/posts/edit.tsx
import { useEffect } from "react";
import { useForm } from "@pankod/refine-react-hook-form";
import { useSelect } from "@pankod/refine-core";

export const PostEdit: React.FC = () => {
const {
refineCore: { onFinish, formLoading, queryResult },
register,
handleSubmit,
resetField,
formState: { errors },
} = useForm();

const { options } = useSelect({
resource: "categories",
defaultValue: queryResult?.data?.data.category.id,
});

useEffect(() => {
resetField("category.id");
}, [options]);

return (
<form onSubmit={handleSubmit(onFinish)}>
<label>Title: </label>
<input {...register("title", { required: true })} />
{errors.title && <span>This field is required</span>}
<br />
<label>Status: </label>
<select {...register("status")}>
<option value="published">published</option>
<option value="draft">draft</option>
<option value="rejected">rejected</option>
</select>
<br />
<label>Category: </label>
<select
{...register("category.id", {
required: true,
})}
defaultValue={queryResult?.data?.data.category.id}
>
{options?.map((category) => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
{errors.category && <span>This field is required</span>}
<br />
<label>Content: </label>
<br />
<textarea
{...register("content", { required: true })}
rows={10}
cols={50}
/>
{errors.content && <span>This field is required</span>}
<br />
<input type="submit" value="Submit" />
{formLoading && <p>Loading</p>}
</form>
);
};
Edit Form

Multipart File Upload

You can submit files or images to your server in multipart/form-data format using the refine-react-hook-form adapter. First of all, let's create a function called onSubmitFile to convert the file from the input to formData type. After placing the selected file in formData, let's upload it to our server. When your form is submitted, the refine onFinish method automatically saves your file and other data on your server.

src/posts/create.tsx
import { useState } from "react";
import { useForm } from "@pankod/refine-react-hook-form";
import { useSelect, useApiUrl } from "@pankod/refine-core";

import axios from "axios";

export const PostCreate: React.FC = () => {
const [isUploading, setIsUploading] = useState<boolean>(false);

const {
refineCore: { onFinish, formLoading },
register,
handleSubmit,
formState: { errors },
setValue,
} = useForm();

const apiURL = useApiUrl();

const { options } = useSelect({
resource: "categories",
});

const onSubmitFile = async () => {
setIsUploading(true);
const inputFile = document.getElementById(
"fileInput",
) as HTMLInputElement;

const formData = new FormData();
formData.append("file", inputFile?.files?.item(0) as File);

const res = await axios.post<{ url: string }>(
`${apiURL}/media/upload`,
formData,
{
withCredentials: false,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
);

setValue("thumbnail", res.data.url);
setIsUploading(false);
};

return (
<form onSubmit={handleSubmit(onFinish)}>
<label>Title: </label>
<input {...register("title", { required: true })} />
{errors.title && <span>This field is required</span>}
<br />
<label>Status: </label>
<select {...register("status")}>
<option value="published">published</option>
<option value="draft">draft</option>
<option value="rejected">rejected</option>
</select>
<br />
<label>Category: </label>
<select
defaultValue={""}
{...register("category.id", { required: true })}
>
<option value={""} disabled>
Please select
</option>
{options?.map((category) => (
<option key={category.value} value={category.value}>
{category.label}
</option>
))}
</select>
{errors.category && <span>This field is required</span>}
<br />
<label>Content: </label>
<br />
<textarea
{...register("content", { required: true })}
rows={10}
cols={50}
/>
{errors.content && <span>This field is required</span>}
<br />
<br />
<label>Image: </label>
<input id="fileInput" type="file" onChange={onSubmitFile} />
<input
type="hidden"
{...register("thumbnail", { required: true })}
/>
{errors.thumbnail && <span>This field is required</span>}
<br />
<br />
<input type="submit" disabled={isUploading} value="Submit" />
{formLoading && <p>Loading</p>}
</form>
);
};
Multipart File Upload

API

Properties

*: These properties have default values in RefineContext and can also be set on the <Refine> component.

External Props

It also accepts all props of useForm hook available in the React Hook Form.


For example, we can define the refineCoreProps property in the useForm hook as:

import { useForm } from "@pankod/refine-react-hook-form";

const { ... } = useForm({
...,
refineCoreProps: {
resource: "posts",
redirect: false,
// You can define all properties provided by refine useForm
},
});

Return values

Returns all the properties returned by React Hook Form of the useForm hook. Also, we added the following return values:

refineCore: Returns all values returned by useForm. You can see all of them in here.

For example, we can access the refineCore return value in the useForm hook as:

import { useForm } from "@pankod/refine-react-hook-form";

const {
refineCore: { queryResult, ... },
} = useForm({ ... });
PropertyDescriptionType
saveButtonPropsProps for a submit button{ disabled: boolean; onClick: (e: React.BaseSyntheticEvent) => void; }

Type Parameters

PropertyDesriptionTypeDefault
TDataResult data of the query. Extends BaseRecordBaseRecordBaseRecord
TErrorCustom error object that extends HttpErrorHttpErrorHttpError
TVariablesField Values for mutation function{}{}
TContextSecond generic type of the useForm of the React Hook Form.{}{}

Live StackBlitz Example