Forms
In almost every user facing application, forms are a necessity. They are the primary way for users to interact with your application and provide data to your backend. They are also one of the most complex parts of an application to build and maintain with many cases and features to consider. Refine's form integration aims to make this process as simple as possible while providing as many real world features as possible out of the box. This guide will cover the basics of forms in Refine and how to use them.
Handling Data
useForm
hook orchestrates Refine's useOne
, useUpdate
and useCreate
hooks internally to provide a single interface for form handling.
While editing or cloning a record, useOne
will be used to fetch the record to provide values for the form. When creating a new record, useCreate
will be used for the mutation. When updating a record, useUpdate
will be used for the mutation.
This means that the useForm
hook will handle all of the data fetching and mutation logic for you. All you need to do is provide the form with the correct props and it will handle the rest.
Basic Usage
The usage of the useForm
hooks may slightly differ between libraries, the core functionality is provided by the @refinedev/core
's useForm
hook and is the same across all implementations. Refine's core has the useForm
hook which is the foundation of all the other extensions and useForm
implementations in the other helper libraries.
To learn more about the usage and see useForm
in action, check out the reference pages for each library:
- Refine's Core
- React Hook Form
- Ant Design
- Mantine
- Material UIReact Hook Form
- Chakra UIReact Hook Form
Refine's Core
import { useForm } from "@refinedev/core";
const EditPage = () => {
const { query, formLoading, onFinish } = useForm<
IProduct,
HttpError,
FormValues
>({
resource: "products",
action: "edit",
id: 123,
});
const record = query.data?.data;
const onSubmit = (event) => {
const data = Object.fromEntries(new FormData(event.target).entries());
onFinish(data);
};
return (
<form onSubmit={onSubmit}>
<label>
Name:
<input defaultValue={record?.name} />
</label>
<label>
Material:
<input defaultValue={record?.material} />
</label>
<button type="submit">Submit</button>
</form>
);
};
Check out Core's useForm
reference page to learn more about the usage and see it in action.
React Hook Form
import { useForm } from "@refinedev/react-hook-form";
const EditPage = () => {
const {
refineCore: { onFinish, formLoading, query },
register,
handleSubmit,
formState: { errors },
saveButtonProps,
} = useForm<IProduct, HttpError, FormValues>({
refineCoreProps: {
resource: "products",
action: "edit",
id: 123,
},
});
return (
<form onSubmit={handleSubmit(onFinish)}>
<label>
Name:
<input {...register("name")} />
</label>
<label>
Material:
<input {...register("material")} />
</label>
<button type="submit">Submit</button>
</form>
);
};
Ant Design
import { useForm, Edit } from "@refinedev/antd";
import { Form, Input } from "antd";
const EditPage = () => {
const { formProps, saveButtonProps, query } = useForm<
IProduct,
HttpError,
FormValues
>({
refineCoreProps: {
resource: "products",
action: "edit",
id: 123,
},
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item label="Name" name="name">
<Input />
</Form.Item>
<Form.Item label="Material" name="material">
<Input />
</Form.Item>
</Form>
</Edit>
);
};
Mantine
import { useForm, Edit } from "@refinedev/mantine";
import { TextInput } from "@mantine/core";
const EditPage = () => {
const {
refineCore: { onFinish, formLoading, query },
register,
handleSubmit,
formState: { errors },
saveButtonProps,
} = useForm<IProduct, HttpError, FormValues>({
refineCoreProps: {
resource: "products",
action: "edit",
id: 123,
},
initialValues: {
name: "",
material: "",
},
});
return (
<Edit saveButtonProps={saveButtonProps}>
<form>
<TextInput
mt={8}
label="Name"
placeholder="Name"
{...getInputProps("name")}
/>
<TextInput
mt={8}
label="Material"
placeholder="Material"
{...getInputProps("material")}
/>
</form>
</Edit>
);
};
Check out Mantine Form's useForm
reference page to learn more about the usage and see it in action.
Material UIReact Hook Form
import { HttpError } from "@refinedev/core";
import { Edit } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Button, Box, TextField } from "@mui/material";
const EditPage = () => {
const {
refineCore: { onFinish, formLoading, query },
register,
handleSubmit,
saveButtonProps,
} = useForm<IProduct, HttpError, FormValues>({
refineCoreProps: {
resource: "products",
action: "edit",
id: 123,
},
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box component="form">
<TextField
{...register("name", {
required: "This field is required",
})}
name="name"
label="Name"
/>
<TextField
{...register("material", {
required: "This field is required",
})}
name="material"
label="Material"
/>
</Box>
</Edit>
);
};
Chakra UIReact Hook Form
import { HttpError } from "@refinedev/core";
import { Edit } from "@refinedev/chakra-ui";
import { useForm } from "@refinedev/react-hook-form";
import { FormControl, FormLabel, Input, Button } from "@chakra-ui/react";
const EditPage = () => {
const {
refineCore: { onFinish, formLoading, query },
register,
handleSubmit,
saveButtonProps,
} = useForm<IProduct, HttpError, FormValues>({
refineCoreProps: {
resource: "products",
action: "edit",
id: 123,
},
});
return (
<Edit saveButtonProps={saveButtonProps}>
<form>
<FormControl mb="3">
<FormLabel>Name</FormLabel>
<Input
id="name"
type="text"
{...register("name", { required: "Name is required" })}
/>
</FormControl>
<FormControl mb="3">
<FormLabel>Material</FormLabel>
<Input
id="material"
type="text"
{...register("material", {
required: "Material is required",
})}
/>
</FormControl>
</form>
</Edit>
);
};
Integration with Routers
If a router integration is made, in most of the cases this enables Refine to infer the resource
, action
and id
from the current route and provide them to useForm
hook. In most of the cases, this will prevent the need of passing explicit resource
, action
and id
props to the hooks including useForm
.
import { useForm } from "@refinedev/core";
useForm({
// These properties will be inferred from the current route
resource: "posts",
action: "edit",
id: 1,
});
To learn more about the routing, check out the Routing guide and the General Concepts guide to learn more about how it benefits the development experience.
Redirection
useForm
also uses the router integration to redirect the user to the desired page after a successful mutation. By default, it's the list page of the resource but this can be customized by passing a redirect
prop to the useForm
hook. If you want to change the redirection behavior for all forms, you can use the options.redirect
prop of the <Refine>
component.
import { useForm } from "@refinedev/core";
useForm({
redirect: "show", // Can also be "list", "edit" or false
});
Unsaved Changes Globally ConfigurableThis value can be configured globally. Click to see the guide for more information.
Refine's useForm
hooks have a built-in feature to prevent the user from losing the unsaved changes via a confirmation dialog when changing the route/leaving the page. To enable this feature, you need to use the <UnsavedChangesNotifier />
components from the router package of the library you are using and set the warnWhenUnsavedChanges
prop to true
.
import { Refine, useForm } from "@refinedev/core";
useForm({
warnWhenUnsavedChanges: true,
});
Usage of <UnsavedChangesNotifier />
- React Router
- Next.js
- Remix
React Router
import { Refine } from "@refinedev/core";
import {
routerProvider,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { BrowserRouter, Routes } from "react-router";
const App = () => (
<BrowserRouter>
<Refine
// ...
routerProvider={routerProvider}
options={{
warnWhenUnsavedChanges: true,
}}
>
<Routes>{/* ... */}</Routes>
{/* The `UnsavedChangesNotifier` component should be placed under <Refine /> component. */}
<UnsavedChangesNotifier />
</Refine>
</BrowserRouter>
);
Check out the UnsavedChangesNotifier
section of the React Router integration documentation for more information.
Next.js
import type { AppProps } from "next/app";
import { Refine } from "@refinedev/core";
import {
routerProvider,
UnsavedChangesNotifier,
} from "@refinedev/nextjs-router/pages";
export default function App({ Component, pageProps }) {
return (
<Refine
// ...
routerProvider={routerProvider}
options={{
warnWhenUnsavedChanges: true,
}}
>
<Component {...pageProps} />
{/* The `UnsavedChangesNotifier` component should be placed under <Refine /> component. */}
<UnsavedChangesNotifier />
</Refine>
);
}
Check out the UnsavedChangesNotifier
section of the React Router integration documentation for more information.
Remix
import type { MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { Refine } from "@refinedev/core";
import routerProvider, {
UnsavedChangesNotifier,
} from "@refinedev/remix-router";
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Refine
// ...
routerProvider={routerProvider}
options={{
warnWhenUnsavedChanges: true,
}}
>
<Outlet />
<UnsavedChangesNotifier />
</Refine>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Check out the UnsavedChangesNotifier
section of the React Router integration documentation for more information.
Actions Router IntegratedThis value can be inferred from the route. Click to see the guide for more information.
In useForm
, you'll have 3 action modes to choose from:
Create
This is the default action mode and is used for creating a new record for the resource.
Edit
Used for editing an existing record. This action mode requires an id
prop to be passed to the form.
Clone
Used for cloning an existing record. This action mode requires an id
prop to be passed to the form. The record with the given id
will be fetched and the values will be used as the initial values for the form fields and the mutation will be performed to create a new record.
Relationships
Refine handles data relations with data hooks(eg: useOne
, useMany
, etc.). This compositional design allows you to easily display other resources' data in your components.
However, when it comes to forms, we may want to add fields that are related to other resources. For instance, you may want to add a category
field to the products
resource. This field will be a select input that will display the categories fetched from the categories
resource. Refine offers useSelect
hook to easily manage select (like a Html <select>
tag, React Select, etc.) components.
You can find more information and usage examples on following useSelect
documentation pages:
In the following example, we will add a category
field to the products
resource. This field will be a select input populated with categories using the useSelect
hook.
- Headless
- Ant Design
- Material UI
- Mantine
Headless
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-hook-form@latest
Content: import React from "react"; import { Refine } from "@refinedev/core"; import dataProvider from "@refinedev/simple-rest"; import { EditPage } from "./edit-page"; const App: React.FC = () => { return ( <Refine dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "posts", }, ]} > <EditPage /> </Refine> ); }; export default App;
Content: import React from "react"; import { useForm } from "@refinedev/react-hook-form"; import { useSelect } from "@refinedev/core"; export const EditPage: React.FC = () => { const { refineCore: { onFinish, formLoading, query: productQuery }, register, handleSubmit, } = useForm<IProduct>({ refineCoreProps: { resource: "products", id: 1, action: "edit", }, }); const product = productQuery?.data?.data; const { options, queryResult: categoriesQueryResult } = useSelect<ICategory>({ resource: "categories", defaultValue: product?.category.id, }); const categories = categoriesQueryResult?.data?.data; // find category of product by id from categories const categoryOfProduct = categories?.find( (category) => Number(category.id) === Number(product?.category.id), ); return ( <div> <div> <h2>{`Edit "${product?.name}" Product`}</h2> <h2>{`Category: ${categoryOfProduct?.title}`}</h2> </div> <form onSubmit={handleSubmit(onFinish)}> <label>Name: </label> <input {...register("name", { required: true })} /> <br /> <label>Category: </label> <select {...register("category.id", { required: true, })} defaultValue={product?.category.id} > {options?.map((category) => { return ( <option key={category.value} value={category.value}> {category.label} </option> ); })} </select> <br /> <br /> <input type="submit" value="Submit" /> {formLoading && <p>Loading</p>} </form> </div> ); }; interface ICategory { id: number; title: string; } interface IProduct { id: number; name: string; category: { id: number }; }
Ant Design
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-hook-form@latest,@refinedev/antd@latest,antd@latest
Content: import React from "react"; import { Refine } from "@refinedev/core"; import { useNotificationProvider, RefineThemes } from "@refinedev/antd"; import dataProvider from "@refinedev/simple-rest"; import { ConfigProvider, App as AntdApp } from "antd"; import "@refinedev/antd/dist/reset.css"; import { EditPage } from "./edit-page"; const API_URL = "https://api.fake-rest.refine.dev"; const App: React.FC = () => { return ( <ConfigProvider theme={RefineThemes.Blue}> <AntdApp> <Refine dataProvider={dataProvider(API_URL)} resources={[ { name: "posts", list: "/posts", show: "/posts/show/:id", create: "/posts/create", edit: "/posts/edit/:id", meta: { canDelete: true, }, }, ]} notificationProvider={useNotificationProvider} options={{ syncWithLocation: true, warnWhenUnsavedChanges: true, }} > <EditPage /> </Refine> </AntdApp> </ConfigProvider> ); }; export default App;
Content: import { useForm, useSelect } from "@refinedev/antd"; import { Form, Input, Select, Button, Row, Col } from "antd"; export const EditPage: React.FC = () => { const { formProps, saveButtonProps, query: productResult, } = useForm<IProduct>({ resource: "products", id: 1, action: "edit", }); const product = productResult?.data?.data; const { selectProps: categorySelectProps, queryResult: categoriesResult } = useSelect<ICategory>({ resource: "categories", defaultValue: product?.category.id, }); const categories = categoriesResult?.data?.data; // find category of product by id from categories const categoryOfProduct = categories?.find( (category) => Number(category.id) === Number(product?.category.id), ); return ( <> <Row justify="center" style={{ paddingTop: 24, paddingBottom: 24, }} > <Col style={{ textAlign: "center", }} > <h2>{`Edit "${product?.name}" Product`}</h2> <h2>{`Category: ${categoryOfProduct?.title}`}</h2> </Col> </Row> <Row justify="center"> <Col span={12}> <Form {...formProps} layout="vertical"> <Form.Item label="Name" name="name" rules={[ { required: true, }, ]} > <Input /> </Form.Item> <Form.Item label="Category" name={["category", "id"]} rules={[ { required: true, }, ]} > <Select {...categorySelectProps} /> </Form.Item> <Button type="primary" {...saveButtonProps}> Save </Button> </Form> </Col> </Row> </> ); }; interface ICategory { id: number; title: string; } interface IProduct { id: number; name: string; category: { id: number }; }
Material UI
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-hook-form@latest,@refinedev/mui@5.0.0,@mui/material@latest
Content: import React from "react"; import { Refine } from "@refinedev/core"; import { RefineThemes, useNotificationProvider, RefineSnackbarProvider, } from "@refinedev/mui"; import CssBaseline from "@mui/material/CssBaseline"; import GlobalStyles from "@mui/material/GlobalStyles"; import { ThemeProvider } from "@mui/material/styles"; import dataProvider from "@refinedev/simple-rest"; import { EditPage } from "./edit-page"; const App: React.FC = () => { return ( <ThemeProvider theme={RefineThemes.Blue}> <CssBaseline /> <GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} /> <RefineSnackbarProvider> <Refine dataProvider={dataProvider( "https://api.fake-rest.refine.dev", )} notificationProvider={useNotificationProvider} resources={[ { name: "posts", }, ]} > <EditPage /> </Refine> </RefineSnackbarProvider> </ThemeProvider> ); }; export default App;
Content: import React from "react"; import { useAutocomplete } from "@refinedev/mui"; import Box from "@mui/material/Box"; import TextField from "@mui/material/TextField"; import Autocomplete from "@mui/material/Autocomplete"; import Button from "@mui/material/Button"; import { useForm } from "@refinedev/react-hook-form"; import { Controller } from "react-hook-form"; import Typography from "@mui/material/Typography"; export const EditPage: React.FC = () => { const { saveButtonProps, refineCore: { query: productQuery }, register, control, } = useForm<IProduct>({ refineCoreProps: { resource: "products", id: 1, action: "edit", }, }); const product = productQuery?.data?.data; const { autocompleteProps, queryResult: categoriesQueryResult } = useAutocomplete<ICategory>({ resource: "categories", defaultValue: product?.category.id, }); const categories = categoriesQueryResult?.data?.data; // find category of product by id from categories const categoryOfProduct = categories?.find( (category) => Number(category.id) === Number(product?.category.id), ); return ( <Box sx={{ display: "flex", alignItems: "center", flexDirection: "column", }} > <Box sx={{ display: "flex", alignItems: "center", flexDirection: "column", py: 2, }} > <Typography variant="h5"> {`Edit "${product?.name}" Product`} </Typography> <Typography variant="h5"> Category: {categoryOfProduct?.title} </Typography> </Box> <Box component="form" sx={{ display: "flex", flexDirection: "column", width: 400 }} autoComplete="off" > <TextField id="name" {...register("name", { required: "This field is required", })} margin="normal" fullWidth label="Name" name="name" autoFocus /> <Controller control={control} name="category" rules={{ required: "This field is required" }} // eslint-disable-next-line defaultValue={null as any} render={({ field }) => ( <Autocomplete<ICategory> id="category" {...autocompleteProps} {...field} onChange={(_, value) => { field.onChange(value); }} getOptionLabel={(item) => { return ( autocompleteProps?.options?.find( (p) => p?.id?.toString() === item?.id?.toString(), )?.title ?? "" ); }} isOptionEqualToValue={(option, value) => value === undefined || option?.id?.toString() === (value?.id ?? value)?.toString() } renderInput={(params) => ( <TextField {...params} label="Category" margin="normal" variant="outlined" required /> )} /> )} /> <Button variant="contained" type="submit" {...saveButtonProps}> Save </Button> </Box> </Box> ); }; interface ICategory { id: number; title: string; } interface IProduct { id: number; name: string; category: { id: number }; }
Mantine
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-hook-form@latest,@refinedev/mantine@^2.28.21,@mantine/core@^5.10.4,@mantine/notifications@^5.10.4,@emotion/react@^11.8.2,@mantine/form@^5.10.4
Content: import React from "react"; import { Refine } from "@refinedev/core"; import { useNotificationProvider, RefineThemes } from "@refinedev/mantine"; import { NotificationsProvider } from "@mantine/notifications"; import { MantineProvider, Global } from "@mantine/core"; import dataProvider from "@refinedev/simple-rest"; import { EditPage } from "./edit-page"; const App: React.FC = () => { return ( <MantineProvider theme={RefineThemes.Blue} withNormalizeCSS withGlobalStyles > <Global styles={{ body: { WebkitFontSmoothing: "auto" } }} /> <NotificationsProvider position="top-right"> <Refine dataProvider={dataProvider( "https://api.fake-rest.refine.dev", )} notificationProvider={useNotificationProvider} resources={[ { name: "posts", list: "/posts", show: "/posts/show/:id", create: "/posts/create", edit: "/posts/edit/:id", meta: { canDelete: true, }, }, ]} options={{ syncWithLocation: true, warnWhenUnsavedChanges: true, }} > <EditPage /> </Refine> </NotificationsProvider> </MantineProvider> ); }; export default App;
Content: import React from "react"; import { useForm, useSelect } from "@refinedev/mantine"; import { Flex, Button, Select, TextInput, Text, Grid } from "@mantine/core"; export const EditPage: React.FC = () => { const { saveButtonProps, getInputProps, refineCore: { query: productQuery }, } = useForm<IProduct>({ initialValues: { name: "", category: { id: "", }, }, refineCoreProps: { resource: "products", id: 1, action: "edit", }, }); const product = productQuery?.data?.data; const { selectProps, queryResult: categoriesQueryResult } = useSelect<ICategory>({ resource: "categories", defaultValue: product?.category.id, }); const categories = categoriesQueryResult?.data?.data; // find category of product by id from categories const categoryOfProduct = categories?.find( (category) => Number(category.id) === Number(product?.category.id), ); return ( <Flex align="center" direction="column" style={{ paddingTop: 24, }} > <Grid> <Grid.Col style={{ textAlign: "center", }} > <Text>{`Edit "${product?.name}" Product`}</Text> <Text>{`Category: ${categoryOfProduct?.title}`}</Text> </Grid.Col> <Grid.Col> <form> <TextInput mt={8} id="name" label="Name" placeholder="Name" {...getInputProps("name")} /> <Select mt={8} id="categoryId" label="Category" placeholder="Pick one" {...getInputProps("category.id")} {...selectProps} /> <Button mt={8} variant="outline" color="blue" {...saveButtonProps} > Save </Button> </form> </Grid.Col> </Grid> </Flex> ); }; interface ICategory { id: number; title: string; } interface IProduct { id: number; name: string; category: { id: number }; }
Mutation Modes Globally ConfigurableThis value can be configured globally. Click to see the guide for more information.
useForm
provides 3 mutation modes to choose from, you may need each of them in different scenarios throughout your application.
useForm({
mutationMode: "optimistic", // Can be "pessimistic", "optimistic" and "undoable". Default is "pessimistic"
});
Pessimistic
This is the default mode and is the most common mode. In this mode, the mutation will be performed immediately and the form will be toggle the loading state until the mutation is completed.
If the mutation fails, the error will be displayed to the user with no further action such as invalidating the cache and redirection after the mutation.
Optimistic
In this mode, the mutation will be performed immediately and simultaneously it will be treated as if it has succeeded. The user will be shown a success notification and the existing query cache will be optimistically updated with the provided form values for the list, many and detail queries.
If not specified the opposite, it will do the redirection to the desired page. If the mutation succeeds, the query cache will be invalidated and the active queries will trigger a refetch.
If the mutation fails, the optimistic updates will be reverted and the error will be displayed to the user.
Undoable
In this mode, the mutation will be delayed for the specified amount of time but simultaneously will be treated as if it has succeeded. Identical to the optimistic
mode, the existing query cache will be updated accordingly and the user will be shown a notification with a countdown.
Unless it is ordered to "undo" the action by the user, the mutation will be performed after the countdown. If the mutation succeeds, the query cache will be invalidated and the active queries will trigger a refetch.
If the mutation fails, the optimistic updates will be reverted and the error will be displayed to the user.
Invalidation
All the queries made by Refine's data hooks and their derivatives are cached for a certain amount of time. This means that if you perform a query for a resource, the result will be cached and the next time you perform the same query, the results will be returned immediately from the cache and then if the data is considered stale, the query will be refetched in the background.
When you perform a mutation, the query cache will be invalidated by default after a successful mutation. This means that if you perform a mutation that affects the data of a query, the query will be refetched in the background and the UI will be updated accordingly.
Default Behavior
By default, useForm
will invalidate the following queries after a successful mutation:
For create
and clone
actions; list
and many
queries for the resource. This means all the related queries made by useList
, useSelect
, useMany
, useTable
etc. will be invalidated.
For edit
action; in addition to the queries invalidated in create
and clone
modes, detail
query for the resource will be invalidated. This means all the related queries made by useOne
, useShow
etc. will be invalidated.
Custom Invalidation
In some cases, you may want to change the default invalidation behavior such as to invalidate all the resource or skipping the list
queries etc. To do that, you can use the invalidates
prop of the useForm
to determine which query sets should be invalidated after a successful mutation.
const { formProps } = useForm({
resource: "posts",
action: "edit",
id: 1,
invalidates: ["many", "detail"], // default is ["list", "many", "detail"]
});
If you want to disable the invalidation completely and handle it manually, you can pass false
to the invalidates
prop. Then, you can use the useInvalidate
hook to invalidate the queries manually based on your conditions.
import { useInvalidate } from "@refinedev/core";
const invalidate = useInvalidate();
useForm({
resource: "categories",
action: "edit",
id: 1,
invalidates: false,
onMutationSuccess() {
invalidate({
resource: "posts",
invalidates: ["resourceAll"],
});
},
});
Optimistic Updates
In many cases, you may want to update the query cache optimistically after a mutation before the mutation is completed. This is especially comes in handy when managing the waiting experience of the user. For example, if you are updating a record, you may want to update the query cache with the new values to show the user that the record is updated immediately and then revert the changes if the mutation fails.
NOTE
Optimistic updates are only available in optimistic
and undoable
mutation modes.
Default Behavior
By default, Refine's mutations will use the provided form data/values to update the existing records in the query cache. This update process includes the list
, many
and detail
queries related to the record and the resource.
Custom Optimistic Updates
In some cases such as the data being submitted is slightly different from the data being fetched in the structural level, you may want to customize the optimistic updates. To do that, you can use the optimisticUpdateMap
prop of the useForm
to determine how the query cache will be updated for each query set.
optimisticUpdateMap
prop also lets you disable the optimistic updates for a specific query set by passing false
to the corresponding key.
useForm({
resource: "posts",
id: 1,
mutationMode: "optimistic",
optimisticUpdateMap: {
list: (
previous, // Previous query data
variables, // Variables used in the query
id, // Record id
) => {
// update the `previous` data using the `variables` and `id`, then return it
},
many: (
previous, // Previous query data
variables, // Variables used in the query
id, // Record id
) => {
// update the `previous` data using the `variables` and `id`, then return it
},
detail: (
previous, // Previous query data
variables, // Variables used in the query
) => {
// update the `previous` data using the `variables`, then return it
},
},
});
Server Side Validation Globally ConfigurableThis value can be configured globally. Click to see the guide for more information.
Server-side form validation is a technique used to validate form data on the server before processing it. Unlike client-side validation, which is performed in the user's browser using JavaScript, server-side validation occurs on the server-side code, typically in the backend of the application.
Refine supports server-side validation out-of-the-box in all useForm
derivatives. To handle server-side validation, the data providers needs to be correctly set up to return the errors in form submissions with a specific format. After this, Refine's useForm
will propagate the errors to the respective form fields.
import { HttpError } from "@refinedev/core";
const error: HttpError = {
message: "An error occurred while updating the record.",
statusCode: 400,
// the errors field is required for server-side validation.
// when the errors field is set, useForm will automatically display the error messages in the form with the corresponding fields.
errors: {
title: ["Title is required"],
content: {
key: "form.error.content",
message: "Content is required.",
},
tags: true,
},
};
Check out HttpError
interface for more information about the error format.
Examples below demonstrates the server-side validation and error propagation:
- Refine's Core
- React Hook Form
- Ant Design
- Mantine
- Material UIReact Hook Form
- Chakra UIReact Hook Form
Refine's Core
Code Example
Dependencies: @refinedev/core@^4.45.1,@refinedev/react-router@latest,@refinedev/simple-rest@^4.5.4,react-dom@^18.0.0,react-router@^7.0.2
Content: import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/react-router"; import { BrowserRouter, Routes, Route, Outlet } from "react-router"; import "./style.css"; import dataProvider from "./data-provider"; import { ProductCreate } from "./create"; const App: React.FC = () => { return ( <BrowserRouter> <Refine routerProvider={routerProvider} dataProvider={dataProvider} resources={[ { name: "products", create: "/products/create", }, ]} options={{ mutationMode: "pessimistic", syncWithLocation: true, redirect: { afterCreate: false } }} > <Routes> <Route path="/products" element={<Outlet />}> <Route path="create" element={<ProductCreate />} /> </Route> </Routes> </Refine> </BrowserRouter> ); }; export default App;
Content: body { margin: 0; padding: 0; font-family: sans-serif; } form { padding: 16px; display: flex; flex-direction: column; gap: 12px; } label { display: flex; flex-direction: row; align-items: flex-start; } label span { display: inline-flex; min-width: 120px; } form button[type="button"] { margin-top: 12px; align-self: flex-end; }
Content: import type { HttpError } from "@refinedev/core"; import baseDataProvider from "@refinedev/simple-rest"; const dataProvider = { ...baseDataProvider("https://api.fake-rest.refine.dev"), create: async () => { // For demo purposes, we're hardcoding the error response. // In a real-world application, the error of the server should match the `HttpError` interface // or should be transformed to match it. return Promise.reject({ message: "This is an error from the server", statusCode: 400, errors: { name: "Name should be at least 3 characters long", material: "Material should start with a capital letter", description: "Description should be at least 10 characters long", }, } as HttpError); } }; export default dataProvider;
Content: import { useForm } from "@refinedev/core"; export const ProductCreate = () => { const { mutation: { error }, formLoading, onFinish, } = useForm(); const { errors } = error ?? {}; return ( <div style={{ position: "relative" }}> <form onSubmit={(event) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const variables = Object.fromEntries(formData.entries()); onFinish(variables).catch(() => {}) }} style={{ display: "flex", flexDirection: "column", gap: "12px" }}> <label> <span>Name</span> <input type="text" id="name" name="name" /> </label> <span style={{ color: "red" }}>{errors?.name ?? ""}</span> <label> <span>Material</span> <input type="text" id="material" name="material" /> </label> <span style={{ color: "red" }}>{errors?.material ?? ""}</span> <label> <span>Description</span> <textarea id="description" name="description" /> </label> <span style={{ color: "red" }}>{errors?.description ?? ""}</span> <button type="submit">Save</button> </form> {formLoading && (<div style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "rgba(255, 255, 255, 0.5)", color: "#000", }}>loading...</div>)} </div> ); };
React Hook Form
Code Example
Dependencies: @tabler/icons-react@^3.1.0,@refinedev/core@^4.45.1,@refinedev/react-router@latest,@refinedev/simple-rest@^4.5.4,@refinedev/react-hook-form@^4.8.12,react-dom@^18.0.0,react-router@^7.0.2,react-hook-form@^7.43.5
Content: import { Refine } from "@refinedev/core"; import routerProvider from "@refinedev/react-router"; import { BrowserRouter, Routes, Route, Outlet } from "react-router"; import "./style.css"; import dataProvider from "./data-provider"; import { ProductCreate } from "./create"; const App: React.FC = () => { return ( <BrowserRouter> <Refine routerProvider={routerProvider} dataProvider={dataProvider} resources={[ { name: "products", create: "/products/create", }, ]} options={{ mutationMode: "pessimistic", syncWithLocation: true, redirect: { afterCreate: false } }} > <Routes> <Route path="/products" element={<Outlet />}> <Route path="create" element={<ProductCreate />} /> </Route> </Routes> </Refine> </BrowserRouter> ); }; export default App;
Content: body { margin: 0; padding: 0; font-family: sans-serif; } form { padding: 16px; display: flex; flex-direction: column; gap: 12px; } label { display: flex; flex-direction: row; align-items: flex-start; } label span { display: inline-flex; min-width: 120px; } form button[type="button"] { margin-top: 12px; align-self: flex-end; }
Content: import type { HttpError } from "@refinedev/core"; import baseDataProvider from "@refinedev/simple-rest"; const dataProvider = { ...baseDataProvider("https://api.fake-rest.refine.dev"), create: async () => { // For demo purposes, we're hardcoding the error response. // In a real-world application, the error of the server should match the `HttpError` interface // or should be transformed to match it. return Promise.reject({ message: "This is an error from the server", statusCode: 400, errors: { name: "Name should be at least 3 characters long", material: "Material should start with a capital letter", description: "Description should be at least 10 characters long", }, } as HttpError); } }; export default dataProvider;
Content: import { useForm } from "@refinedev/react-hook-form"; export const ProductCreate = () => { const { refineCore: { formLoading }, saveButtonProps, register, formState: { errors }, } = useForm(); return ( <div style={{ position: "relative" }}> <form style={{ display: "flex", flexDirection: "column", gap: "12px" }}> <label> <span>Name</span> <input type="text" id="name" {...register("name")} /> </label> <span style={{ color: "red" }}>{errors?.name?.message}</span> <label> <span>Material</span> <input type="text" id="material" {...register("material")} /> </label> <span style={{ color: "red" }}>{errors?.material?.message}</span> <label> <span>Description</span> <textarea id="description" {...register("description")} /> </label> <span style={{ color: "red" }}>{errors?.description?.message}</span> <button type="button" {...saveButtonProps}>Save</button> </form> {formLoading && (<div style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "rgba(255, 255, 255, 0.5)", color: "#000", }}>loading...</div>)} </div> ); };
Ant Design
Code Example
Dependencies: @refinedev/antd@latest,@refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-router@latest,react-router@^7.0.2,antd@^5.0.5
Content: import React from "react"; import { Refine, Authenticated } from "@refinedev/core"; import routerProvider, { NavigateToResource } from "@refinedev/react-router"; import { BrowserRouter, Route, Routes, Outlet, Navigate } from "react-router"; import { ErrorComponent, RefineThemes, ThemedLayoutV2, useNotificationProvider, AuthPage } from "@refinedev/antd"; import { App as AntdApp, ConfigProvider } from "antd"; import dataProvider from "./data-provider"; import "@refinedev/antd/dist/reset.css"; import { ProductShow } from "./show"; import { ProductCreate } from "./create"; export default function App() { return ( <BrowserRouter> <ConfigProvider theme={RefineThemes.Blue}> <AntdApp> <Refine routerProvider={routerProvider} dataProvider={dataProvider} notificationProvider={useNotificationProvider} resources={[ { name: "products", create: "/products/create" } ]} options={{ mutationMode: "pessimistic", syncWithLocation: true, redirect: { afterCreate: false } }} > <Routes> <Route element={ <ThemedLayoutV2> <Outlet /> </ThemedLayoutV2> } > <Route path="/products" element={<Outlet />}> <Route path="create" element={<ProductCreate />} /> </Route> <Route path="*" element={<ErrorComponent />} /> </Route> </Routes> </Refine> </AntdApp> </ConfigProvider> </BrowserRouter> ); };
Content: import type { HttpError } from "@refinedev/core"; import baseDataProvider from "@refinedev/simple-rest"; const dataProvider = { ...baseDataProvider("https://api.fake-rest.refine.dev"), create: async () => { // For demo purposes, we're hardcoding the error response. // In a real-world application, the error of the server should match the `HttpError` interface // or should be transformed to match it. return Promise.reject({ message: "This is an error from the server", statusCode: 400, errors: { name: "Name should be at least 3 characters long", material: "Material should start with a capital letter", description: "Description should be at least 10 characters long", }, } as HttpError); } }; export default dataProvider;
Content: import React from "react"; import { Typography, Form, Input, InputNumber } from "antd"; import { Create, useForm } from "@refinedev/antd"; const { Title } = Typography; const { TextArea } = Input; export const ProductCreate = () => { const { formProps, saveButtonProps } = useForm({ refineCoreProps: { redirect: "show" }}); return ( <Create saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> <Form.Item label="Name" name="name" > <Input /> </Form.Item> <Form.Item label="Material" name="material" > <Input /> </Form.Item> <Form.Item label="Description" name="description" > <TextArea rows={2} /> </Form.Item> </Form> </Create> ); };
Mantine
Code Example
Dependencies: @refinedev/mantine@^2.28.21,@refinedev/core@^4.45.1,@refinedev/react-router@^latest,@refinedev/simple-rest@^4.5.4,@refinedev/react-table@^5.6.4,@tanstack/react-table@^8.2.6,@tabler/icons-react@^3.1.0,@emotion/react@^11.8.2,@mantine/core@^5.10.4,@mantine/hooks@^5.10.4,@mantine/form@^5.10.4,@mantine/notifications@^5.10.4,react-router@^7.0.2
Content: import { Refine, Authenticated } from "@refinedev/core"; import { ErrorComponent, ThemedLayoutV2, RefineThemes, useNotificationProvider, AuthPage } from "@refinedev/mantine"; import { NotificationsProvider } from "@mantine/notifications"; import { MantineProvider, Global } from "@mantine/core"; import routerProvider, { NavigateToResource, } from "@refinedev/react-router"; import { BrowserRouter, Routes, Route, Outlet, Navigate } from "react-router"; import dataProvider from "./data-provider"; import { ProductCreate } from "./create"; const App: React.FC = () => { return ( <BrowserRouter> <MantineProvider theme={RefineThemes.Blue} withNormalizeCSS withGlobalStyles > <Global styles={{ body: { WebkitFontSmoothing: "auto" } }} /> <NotificationsProvider position="top-right"> <Refine notificationProvider={useNotificationProvider} routerProvider={routerProvider} dataProvider={dataProvider} resources={[ { name: "products", create: "/products/create", }, ]} options={{ mutationMode: "pessimistic", syncWithLocation: true, redirect: { afterCreate: false } }} > <Routes> <Route element={ <ThemedLayoutV2> <Outlet /> </ThemedLayoutV2> } > <Route path="/products" element={<Outlet />}> <Route path="create" element={<ProductCreate />} /> </Route> <Route path="*" element={<ErrorComponent />} /> </Route> </Routes> </Refine> </NotificationsProvider> </MantineProvider> </BrowserRouter> ); }; export default App;
Content: import type { HttpError } from "@refinedev/core"; import baseDataProvider from "@refinedev/simple-rest"; const dataProvider = { ...baseDataProvider("https://api.fake-rest.refine.dev"), create: async () => { // For demo purposes, we're hardcoding the error response. // In a real-world application, the error of the server should match the `HttpError` interface // or should be transformed to match it. return Promise.reject({ message: "This is an error from the server", statusCode: 400, errors: { name: "Name should be at least 3 characters long", material: "Material should start with a capital letter", description: "Description should be at least 10 characters long", }, } as HttpError); } }; export default dataProvider;
Content: import { Create, useForm } from "@refinedev/mantine"; import { TextInput, Textarea, NumberInput } from "@mantine/core"; export const ProductCreate = () => { const { saveButtonProps, getInputProps, errors, } = useForm({ initialValues: { name: "", material: "", }, }); return ( <Create saveButtonProps={saveButtonProps}> <form> <TextInput mt={8} id="name" label="Name" placeholder="Name" {...getInputProps("name")} /> <TextInput mt={8} id="material" label="Material" placeholder="Material" {...getInputProps("material")} /> <Textarea mt={8} id="description" label="Description" placeholder="Description" {...getInputProps("description")} /> </form> </Create> ); };
Material UIReact Hook Form
Code Example
Dependencies: @refinedev/mui@latest,@refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-router@latest,@refinedev/react-hook-form@^4.8.12,@emotion/react@^11.8.2,@emotion/styled@^11.8.1,@mui/lab@^6.0.0-beta.14,@mui/material@^6.1.7,@mui/system@latest,@mui/x-data-grid@^7.22.2,react-router@^7.0.2,react-hook-form@^7.43.5
Content: import { Refine, Authenticated } from "@refinedev/core"; import routerProvider, { NavigateToResource } from "@refinedev/react-router"; import { BrowserRouter, Route, Routes, Outlet, Navigate } from "react-router"; import { ThemedLayoutV2, ErrorComponent, RefineThemes, useNotificationProvider, RefineSnackbarProvider, AuthPage, } from "@refinedev/mui"; import CssBaseline from "@mui/material/CssBaseline"; import GlobalStyles from "@mui/material/GlobalStyles"; import { ThemeProvider } from "@mui/material/styles"; import dataProvider from "./data-provider"; import { ProductCreate } from "./create"; export default function App() { return ( <BrowserRouter> <ThemeProvider theme={RefineThemes.Blue}> <CssBaseline /> <GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} /> <RefineSnackbarProvider> <Refine routerProvider={routerProvider} dataProvider={dataProvider} notificationProvider={useNotificationProvider} resources={[ { name: "products", create: "/products/create", }, ]} options={{ mutationMode: "pessimistic", syncWithLocation: true, redirect: { afterCreate: false } }} > <Routes> <Route element={ <ThemedLayoutV2> <Outlet /> </ThemedLayoutV2> } > <Route path="/products" element={<Outlet />}> <Route path="create" element={<ProductCreate />} /> </Route> <Route path="*" element={<ErrorComponent />} /> </Route> </Routes> </Refine> </RefineSnackbarProvider> </ThemeProvider> </BrowserRouter> ); };
Content: import type { HttpError } from "@refinedev/core"; import baseDataProvider from "@refinedev/simple-rest"; const dataProvider = { ...baseDataProvider("https://api.fake-rest.refine.dev"), create: async () => { // For demo purposes, we're hardcoding the error response. // In a real-world application, the error of the server should match the `HttpError` interface // or should be transformed to match it. return Promise.reject({ message: "This is an error from the server", statusCode: 400, errors: { name: "Name should be at least 3 characters long", material: "Material should start with a capital letter", description: "Description should be at least 10 characters long", }, } as HttpError); } }; export default dataProvider;
Content: import { HttpError } from "@refinedev/core"; import { Create, useAutocomplete } from "@refinedev/mui"; import Box from "@mui/material/Box"; import TextField from "@mui/material/TextField"; import Autocomplete from "@mui/material/Autocomplete"; import { useForm } from "@refinedev/react-hook-form"; import { Controller } from "react-hook-form"; export const ProductCreate = () => { const { saveButtonProps, refineCore: { query, autoSaveProps }, register, control, formState: { errors }, } = useForm(); return ( <Create saveButtonProps={saveButtonProps}> <Box component="form" sx={{ display: "flex", flexDirection: "column" }} autoComplete="off" > <TextField id="name" {...register("name")} error={!!errors.name} helperText={errors.name?.message} margin="normal" fullWidth label="Name" name="name" autoFocus /> <TextField id="material" {...register("material")} error={!!errors.material} helperText={errors.material?.message} margin="normal" fullWidth label="Material" name="material" autoFocus /> <TextField id="description" {...register("description")} error={!!errors.description} helperText={errors.description?.message} margin="normal" label="Description" multiline rows={4} /> </Box> </Create> ); };
Chakra UIReact Hook Form
Code Example
Dependencies: @refinedev/chakra-ui@^2.26.17,@tabler/icons-react@^3.1.0,@refinedev/core@^4.45.1,@refinedev/react-router@latest,@refinedev/simple-rest@^4.5.4,@refinedev/react-table@^5.6.4,@tanstack/react-table@^8.2.6,@refinedev/react-hook-form@^4.8.12,@chakra-ui/react@^2.5.1,react-dom@^18.0.0,react-router@^7.0.2,react-hook-form@^7.43.5
Content: import { Refine } from "@refinedev/core"; import { ErrorComponent, ThemedLayoutV2, RefineThemes, useNotificationProvider, AuthPage } from "@refinedev/chakra-ui"; import { ChakraProvider } from "@chakra-ui/react"; import routerProvider, { NavigateToResource, } from "@refinedev/react-router"; import { BrowserRouter, Routes, Route, Outlet, Navigate } from "react-router"; import dataProvider from "./data-provider"; import { ProductCreate } from "./create"; const App: React.FC = () => { return ( <BrowserRouter> <ChakraProvider theme={RefineThemes.Blue}> <Refine notificationProvider={useNotificationProvider} routerProvider={routerProvider} dataProvider={dataProvider} resources={[ { name: "products", create: "/products/create", }, ]} options={{ mutationMode: "pessimistic", syncWithLocation: true, redirect: { afterCreate: false } }} > <Routes> <Route element={ <ThemedLayoutV2> <Outlet /> </ThemedLayoutV2> } > <Route path="/products" element={<Outlet />}> <Route path="create" element={<ProductCreate />} /> </Route> <Route path="*" element={<ErrorComponent />} /> </Route> </Routes> </Refine> </ChakraProvider> </BrowserRouter> ); }; export default App;
Content: import type { HttpError } from "@refinedev/core"; import baseDataProvider from "@refinedev/simple-rest"; const dataProvider = { ...baseDataProvider("https://api.fake-rest.refine.dev"), create: async () => { // For demo purposes, we're hardcoding the error response. // In a real-world application, the error of the server should match the `HttpError` interface // or should be transformed to match it. return Promise.reject({ message: "This is an error from the server", statusCode: 400, errors: { name: "Name should be at least 3 characters long", material: "Material should start with a capital letter", description: "Description should be at least 10 characters long", }, } as HttpError); } }; export default dataProvider;
Content: import { Create } from "@refinedev/chakra-ui"; import { FormControl, FormErrorMessage, FormLabel, Input, Textarea, } from "@chakra-ui/react"; import { useForm } from "@refinedev/react-hook-form"; export const ProductCreate = () => { const { refineCore: { formLoading }, saveButtonProps, register, formState: { errors }, } = useForm(); return ( <Create isLoading={formLoading} saveButtonProps={saveButtonProps}> <FormControl mb="3" isInvalid={!!errors?.name}> <FormLabel>Name</FormLabel> <Input id="name" type="text" {...register("name")} /> <FormErrorMessage> {`${errors.name?.message}`} </FormErrorMessage> </FormControl> <FormControl mb="3" isInvalid={!!errors?.material}> <FormLabel>Material</FormLabel> <Input id="material" type="text" {...register("material")} /> <FormErrorMessage> {`${errors.material?.message}`} </FormErrorMessage> </FormControl> <FormControl mb="3" isInvalid={!!errors?.description}> <FormLabel>Description</FormLabel> <Textarea id="description" {...register("description")} /> <FormErrorMessage> {`${errors.description?.message}`} </FormErrorMessage> </FormControl> </Create> ); };
Notifications
When forms are submitted, it is a good practice to notify the user about the result of the submission. useForm
handles this for you, when the mutation succeeds or fails it will show a notification to the user with a proper message. This behavior can be customized or disabled using the successNotification
and errorNotification
props.
These props accepts both a function that returns the configuration or a static configuration, this means you'll be able to use the response of the mutation to customize the notification message.
useForm({
// If not passed explicitly, these default values will be used. Default values can also be customized via i18n.
successNotification: (data, values, resource) => {
return {
description: translate("notifications.success", "Successful"),
message: translate(
"notifications.(edit|create)Success",
"Successfully (updated|created) {resource}",
),
type: "success",
};
},
// If not passed explicitly, these default values will be used. Default values can also be customized via i18n.
errorNotification: (error, values, resource) => {
return {
description: error.message,
message: translate(
"notifications.(edit|create)Error",
"Error when (updating|creating) {resource} (status code: {error.statusCode})",
),
type: "error",
};
},
});
Auto Save
In many forms, it is a good practice to save the form data automatically as the user types to avoid losing the data in case of an unexpected event. This is especially useful in long forms where the user may spend a lot of time filling the form. useForm
is packed with this feature out-of-the-box.
While @refinedev/core
's useForm
packs this feature, the auto save is not triggered automatically. In the extensions of the useForm
hook in the other libraries, the auto save is handled internally and is triggered automatically.
import { useForm } from "@refinedev/core";
const { autoSaveProps } = useForm({
autoSave: {
enabled: true, // Enables the auto save feature, defaults to false
debounce: 2000, // Debounce interval to trigger the auto save, defaults to 1000
invalidateOnUnmount: true, // Invalidates the queries when the form is unmounted, defaults to false
},
});
<AutoSaveIndicator />
Refine's core and ui integrations are shipped with an <AutoSaveIndicator />
component that can be used to show a visual indicator to the user when the auto save is triggered. The autoSaveProps
value from the useForm
's return value can be passed to the <AutoSaveIndicator />
to show the auto save status to the user. It will automatically show the loading, success and error states to the user.
import { AutoSaveIndicator } from "@refinedev/core";
const { autoSaveProps } = useForm({
resource: "posts",
action: "edit",
id: 1,
autoSave: {
enabled: true,
},
});
return (
<form>
{/* ... */}
<AutoSaveIndicator {...autoSaveProps} />
</form>
);
Modifying Data Before Submission
In some cases, you might want to change the data before submitting it to the backend. For example, you might want to add a full_name
field to the form data of a user resource by combining the first_name
and last_name
fields. While the useForm
from the @refinedev/core
has the natural support for this, the useForm
derivatives from the other libraries of Refine has a different approach.
Each of these form implementations have a way to modify the data before submission with a slightly different approach. To learn more about how to modify the data before submission, check out the usage examples of each library:
- React Hook Form
- Ant Design
- Mantine
React Hook Form
To learn more about how to modify the data before submission, check out the Using useForm
of @refinedev/react-hook-form
reference page.
- Headless
- With Material UI
- With Chakra UI
Headless
import { useForm } from "@refinedev/react-hook-form";
import { FieldValues } from "react-hook-form";
const EditPage = () => {
const {
refineCore: { onFinish },
register,
handleSubmit,
} = useForm();
const onFinishHandler = (data: FieldValues) => {
onFinish({
fullName: `${data.name} ${data.surname}`,
});
};
return (
<form onSubmit={handleSubmit(onFinishHandler)}>
<label>
Name:
<input {...register("name")} />
</label>
<label>
Surname:
<input {...register("surname")} />
</label>
<button type="submit">Submit</button>
</form>
);
};
With Material UI
import { HttpError } from "@refinedev/core";
import { Create } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Button, Box, TextField } from "@mui/material";
type FormValues = {
name: string;
surname: string;
};
export const UserCreate: React.FC = () => {
const {
saveButtonProps,
refineCore: { onFinish },
handleSubmit,
} = useForm<FormValues, HttpError, FormValues>();
const handleSubmitPostCreate = (values: FormValues) => {
const { name, surname } = values;
const fullName = `${name} ${surname}`;
onFinish({
...value,
fullName,
});
};
return (
<Create
saveButtonProps={{
...saveButtonProps,
onClick: handleSubmit(handleSubmitForm),
}}
>
<Box component="form">
<TextField
{...register("name", {
required: "This field is required",
})}
name="name"
label="Name"
error={!!errors.name}
helperText={errors.name?.message}
/>
<TextField
{...register("surname", {
required: "This field is required",
})}
name="surname"
label="Surname"
error={!!errors.surname}
helperText={errors.surname?.message}
/>
</Box>
</Create>
);
};
Check out the <Create />
component and saveButtonProps
prop to learn more about their usage.
With Chakra UI
import { HttpError } from "@refinedev/core";
import { Create } from "@refinedev/chakra-ui";
import { useForm } from "@refinedev/react-hook-form";
import { FormControl, FormLabel, Input, Button } from "@chakra-ui/react";
type FormValues = {
name: string;
surname: string;
};
export const UserCreate: React.FC = () => {
const {
saveButtonProps,
refineCore: { onFinish },
handleSubmit,
} = useForm<FormValues, HttpError, FormValues>();
const handleSubmitPostCreate = (values: FormValues) => {
const { name, surname } = values;
const fullName = `${name} ${surname}`;
onFinish({
...value,
fullName,
});
};
return (
<Create
saveButtonProps={{
...saveButtonProps,
onClick: handleSubmit(handleSubmitForm),
}}
>
<form>
<FormControl mb="3">
<FormLabel>Name</FormLabel>
<Input
id="name"
type="text"
{...register("name", { required: "Name is required" })}
/>
</FormControl>
<FormControl mb="3">
<FormLabel>Surname</FormLabel>
<Input
id="surname"
type="text"
{...register("surname", {
required: "Surname is required",
})}
/>
</FormControl>
</form>
</Create>
);
};
Check out the <Create />
component and saveButtonProps
prop to learn more about their usage.
Ant Design
To learn more about how to modify the data before submission, check out the Using useForm
of @refinedev/antd
reference page.
import { useForm, Create } from "@refinedev/antd";
import { Form, Input } from "antd";
const EditPage = () => {
const { formProps, saveButtonProps, onFinish } = useForm();
const handleOnFinish = (values) => {
onFinish({
fullName: `${values.name} ${values.surname}`,
});
};
return (
<Create saveButtonProps={saveButtonProps}>
<Form {...formProps} onFinish={handleOnFinish} layout="vertical">
<Form.Item label="Name" name="name">
<Input />
</Form.Item>
<Form.Item label="Surname" name="surname">
<Input />
</Form.Item>
</Form>
</Create>
);
};
Mantine
To learn more about how to modify the data before submission, check out the Using useForm
of @refinedev/mantine
reference page.
import { useForm, Create } from "@refinedev/mantine";
import { TextInput } from "@mantine/core";
const CreatePage = () => {
const { saveButtonProps, getInputProps } = useForm({
initialValues: {
name: "",
surname: "",
},
transformValues: (values) => ({
fullName: `${values.name} ${values.surname}`,
}),
});
return (
<Create saveButtonProps={saveButtonProps}>
<form>
<TextInput
mt={8}
label="Name"
placeholder="Name"
{...getInputProps("name")}
/>
<TextInput
mt={8}
label="Surname"
placeholder="Surname"
{...getInputProps("surname")}
/>
</form>
</Create>
);
};
Save and Continue
In many cases, you may want to redirect the user to the edit page of the record after creating it. This is especially useful in cases where the user needs to fill a long form and you don't want to lose the data in case of an unexpected event.
In the example below, we'll create multiple options for the user to choose from after creating a record. The user will be able to choose between redirecting to the list page, edit page or staying in the create page in order to continue creating records.
Code Example
Dependencies: @refinedev/core@latest,@refinedev/simple-rest@latest,@refinedev/react-router@latest,@refinedev/react-hook-form@latest,react-router@^7.0.2
Content: import React from "react"; import { Refine } from "@refinedev/core"; import dataProvider from "@refinedev/simple-rest"; import { BrowserRouter, Route, Routes, Navigate, Outlet } from "react-router"; import routerProvider from "@refinedev/react-router"; import "./style.css"; import { List } from "./list.tsx"; import { Edit } from "./edit.tsx"; import { Create } from "./create.tsx"; export default function App() { return ( <BrowserRouter> <Refine routerProvider={routerProvider} dataProvider={dataProvider("https://api.fake-rest.refine.dev")} resources={[ { name: "products", list: "/products", create: "/products/create", edit: "/products/edit/:id", } ]} > <Routes> <Route path="/products" element={<Outlet />}> <Route index element={<List />} /> <Route path="create" element={<Create />} /> <Route path="edit/:id" element={<Edit />} /> </Route> </Routes> </Refine> </BrowserRouter> ); }
Content: html { margin: 0; padding: 0; } body { margin: 0; padding: 12px; } * { box-sizing: border-box; } body { font-family: sans-serif; } form label, form input, form button { display: block; width: 100%; margin-bottom: 6px; }
Content: import { useList, BaseKey } from "@refinedev/core"; import { Link } from "react-router"; export const List: React.FC = () => { const { data, isLoading, isError } = useList<IProduct>({ resource: "products", filters: [ { field: "id", operator: "gte", value: 120, } ] }); if (isLoading) { return <div>Loading...</div>; } return ( <div> <h1>Products</h1> <Link to="/products/create">Create Product</Link> <ul> {data?.data?.map((product) => ( <li key={product.id}> {product.name} </li> ))} </ul> </div> ); }; interface IProduct { id: BaseKey; name: string; material: string; }
Content: import React from "react"; import { useForm } from "@refinedev/react-hook-form"; import type { HttpError, BaseKey } from "@refinedev/core"; export const Create: React.FC = () => { const { refineCore: { onFinish, formLoading, redirect }, register, handleSubmit, reset, } = useForm<IProduct, HttpError, FormValues>({ refineCoreProps: { redirect: false, } }); const saveAndList = (variables: FormValues) => { onFinish(variables).then(() => { // The default behavior is (unless changed in <Refine /> component) redirecting to the list page. // Since we've stated as `redirect: false` in the useForm hook, we need to redirect manually. redirect("list"); }); }; const saveAndContinue = (variables: FormValues) => { onFinish(variables).then(({ data }) => { // We'll wait for the mutation to finish and grab the id of the created product from the response. // This will only work on `pesimistic` mutation mode. redirect("edit", data.id); }); }; const saveAndAddAnother = (variables: FormValues) => { onFinish(variables).then(() => { // We'll wait for the mutation to finish and reset the form. reset(); }); }; return ( <div> <h1>Create Product</h1> <form onSubmit={handleSubmit(saveAndList)}> <label htmlFor="name">Name</label> <input name="name" placeholder="Name" {...register("name", { required: true })} /> <label htmlFor="material">Material</label> <input name="material" placeholder="Material" {...register("material", { required: true })} /> <div style={{ display: "flex", gap: "12px" }}> <button type="submit">Save</button> <button type="button" onClick={handleSubmit(saveAndContinue)}>Save and Continue Editing</button> <button type="button" onClick={handleSubmit(saveAndAddAnother)}>Save and Add Another</button> </div> </form> </div> ); }; interface IProduct { id: BaseKey; name: string; material: string; } interface FormValues { name?: string; material?: string; }
Content: import React from "react"; import { useSelect } from "@refinedev/core"; import { useForm } from "@refinedev/react-hook-form"; import type { HttpError, BaseKey } from "@refinedev/core"; export const Edit: React.FC = () => { const { refineCore: { onFinish, formLoading }, register, handleSubmit, reset } = useForm<IProduct, HttpError, FormValues>(); return ( <div> <h1>Edit Product</h1> <form onSubmit={handleSubmit(onFinish)}> <label htmlFor="name">Name</label> <input name="name" placeholder="Name" {...register("name", { required: true })} /> <label htmlFor="material">Material</label> <input name="material" placeholder="Material" {...register("material", { required: true })} /> <button type="submit">Save</button> </form> </div> ); }; interface IProduct { id: BaseKey; name: string; material: string; } interface FormValues { name?: string; material?: string; }
- Handling Data
- Basic Usage
- Integration with Routers
- Redirection
- Unsaved Changes
- Usage of
<UnsavedChangesNotifier />
- Actions
- Create
- Edit
- Clone
- Relationships
- Mutation Modes
- Pessimistic
- Optimistic
- Undoable
- Invalidation
- Default Behavior
- Custom Invalidation
- Optimistic Updates
- Default Behavior
- Custom Optimistic Updates
- Server Side Validation
- Notifications
- Auto Save
<AutoSaveIndicator />
- Modifying Data Before Submission
- Save and Continue