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.
export const PostList: React.FC = () => {
return <></>;
};
export const PostCreate: React.FC = () => {
return <></>;
};
export const PostEdit: React.FC = () => {
return <></>;
};
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.
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>
);
};
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. →
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>
);
};
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.
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>
);
};
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.
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>
);
};
API
Properties
*
: These properties have default values inRefineContext
and can also be set on the <Refine> component.
It also accepts all props of useForm hook available in the React Hook Form.
For example, we can define the
refineCoreProps
property in theuseForm
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 theuseForm
hook as:
import { useForm } from "@pankod/refine-react-hook-form";
const {
refineCore: { queryResult, ... },
} = useForm({ ... });
Property | Description | Type |
---|---|---|
saveButtonProps | Props for a submit button | { disabled: boolean; onClick: (e: React.BaseSyntheticEvent) => void; } |
Type Parameters
Property | Desription | Type | Default |
---|---|---|---|
TData | Result data of the query. Extends BaseRecord | BaseRecord | BaseRecord |
TError | Custom error object that extends HttpError | HttpError | HttpError |
TVariables | Field Values for mutation function | {} | {} |
TContext | Second generic type of the useForm of the React Hook Form. | {} | {} |