Skip to main content
useForm orchestrates Refine’s data hooks to create, edit, and clone data. It provides a set of features to make it easier to implement real world needs and handle edge cases such as redirects, invalidation, auto-save and more.

Usage

import { useForm } from "@refinedev/core";

const { onFinish, mutation, query, formLoading, redirect } = useForm({
  resource: "posts",
  action: "create",
});

Props

resource
string
Resource name for API data interactions.Default: Resource name that it reads from route
action
'create' | 'edit' | 'clone'
Type of the form mode.Default: Action that it reads from route otherwise "create" is used
id
BaseKey
Record id for fetching. Required for edit and clone actions.Default: Id that it reads from the URL
redirect
'show' | 'edit' | 'list' | 'create' | false
Page to redirect after a successful mutation.Default: "list"
meta
MetaQuery
Metadata query for dataProvider.
queryMeta
MetaQuery
Metadata to pass for the useOne query.
mutationMeta
MetaQuery
Metadata to pass for the mutation (useCreate for create and clone actions, useUpdate for edit action).
mutationMode
'pessimistic' | 'optimistic' | 'undoable'
Determines when mutations are executed (pessimistic, optimistic, or undoable).Default: "pessimistic"
onMutationSuccess
(data, variables, context, isAutoSave) => void
Called when a mutation is successful.
onMutationSuccess: (data, variables, context, isAutoSave) => {
  console.log("Form submitted successfully", data);
}
onMutationError
(error, variables, context, isAutoSave) => void
Called when a mutation encounters an error.
onMutationError: (error, variables, context, isAutoSave) => {
  console.error("Form submission failed", error);
}
undoableTimeout
number
Duration to wait before executing mutations when mutationMode = "undoable".Default: 5000
dataProviderName
string
If there is more than one dataProvider, you should use the dataProviderName that you will use.
invalidates
Array<'all' | 'resourceAll' | 'list' | 'many' | 'detail' | false>
You can use it to manage the invalidations that will occur at the end of the mutation.Default: ["list", "many", "detail"]
queryOptions
UseQueryOptions
react-query’s useQuery options of useOne hook used while in edit mode.
createMutationOptions
UseMutationOptions
react-query’s useMutation options of useCreate hook used while submitting in create and clone modes.
updateMutationOptions
UseMutationOptions
react-query’s useMutation options of useUpdate hook used while submitting in edit mode.
optimisticUpdateMap
OptimisticUpdateMapType
If you customize the optimisticUpdateMap option, you can use it to manage the invalidations that will occur at the end of the mutation.Default:
{
  list: true,
  many: true,
  detail: true,
}
successNotification
OpenNotificationParams | false
Notification configuration for successful mutations.
errorNotification
OpenNotificationParams | false
Notification configuration for failed mutations.
liveMode
'auto' | 'manual' | 'off'
Whether to update data automatically or manually if a related live event is received.
onLiveEvent
(event: LiveEvent) => void
Callback to handle live events.
liveParams
object
Additional params to pass to liveProvider’s subscribe method.
autoSave
object
Auto-save configuration. Only works in edit mode.
autoSave.enabled
boolean
required
Enable auto-save feature.
autoSave.debounce
number
default:"1000"
Debounce time in milliseconds before triggering auto-save.
autoSave.onFinish
(values: TVariables) => TVariables
Transform values before auto-save.
autoSave.invalidateOnUnmount
boolean
Whether to invalidate queries when component unmounts.
autoSave.invalidateOnClose
boolean
Whether to invalidate queries when modal/drawer closes.
overtimeOptions
object
Loading overtime configuration.
overtimeOptions.interval
number
default:"1000"
Interval in milliseconds.
overtimeOptions.onInterval
(elapsedTime: number) => void
Callback to handle interval events.

Return Values

onFinish
(values: TVariables) => Promise<CreateResponse | UpdateResponse | void>
A function to submit the form. Returns a promise that resolves with the mutation result.
const handleSubmit = async (values) => {
  try {
    await onFinish(values);
    console.log("Success!");
  } catch (error) {
    console.error("Error:", error);
  }
};
onFinishAutoSave
(values: TVariables) => Promise<UpdateResponse | void>
A debounced function for auto-save. Only available when autoSave.enabled is true.
mutation
UseMutationResult
Result of the react-query’s useMutation. Either from useCreate or useUpdate depending on the action.
query
QueryObserverResult<GetOneResponse<TData>, TError>
Result of the react-query’s useQuery from useOne. Only available in edit and clone modes.
formLoading
boolean
Loading state that combines mutation loading and query fetching states.
id
BaseKey | undefined
Record id for fetching.
setId
React.Dispatch<React.SetStateAction<BaseKey | undefined>>
A function to set the record id.
redirect
(redirect, idFromFunction?, routeParams?) => void
A function to redirect to a specific page.
// Redirect to list page
redirect("list");

// Redirect to edit page with specific id
redirect("edit", "123");

// Redirect to show page with route params
redirect("show", "123", { tab: "comments" });
autoSaveProps
object
Auto-save status information.
autoSaveProps.status
'idle' | 'loading' | 'success' | 'error'
Current auto-save status.
autoSaveProps.data
UpdateResponse | undefined
Data from the last successful auto-save.
autoSaveProps.error
TResponseError | null
Error from the last failed auto-save.
overtime
object
Overtime loading information.
overtime.elapsedTime
number
Elapsed time in milliseconds.

Example

Create Form

import { useForm } from "@refinedev/core";

const PostCreate = () => {
  const { onFinish, mutation, formLoading } = useForm({
    resource: "posts",
    action: "create",
    redirect: "edit",
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const values = Object.fromEntries(formData.entries());
    onFinish(values);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Title:
        <input name="title" type="text" required />
      </label>
      
      <label>
        Content:
        <textarea name="content" required />
      </label>
      
      <button type="submit" disabled={formLoading}>
        {formLoading ? "Saving..." : "Create Post"}
      </button>
    </form>
  );
};

Edit Form

import { useForm } from "@refinedev/core";

const PostEdit = () => {
  const { onFinish, query, formLoading } = useForm({
    resource: "posts",
    action: "edit",
    id: "1",
  });

  const post = query?.data?.data;

  if (!post && formLoading) {
    return <div>Loading...</div>;
  }

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const values = Object.fromEntries(formData.entries());
    onFinish(values);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Title:
        <input name="title" type="text" defaultValue={post?.title} required />
      </label>
      
      <label>
        Content:
        <textarea name="content" defaultValue={post?.content} required />
      </label>
      
      <button type="submit" disabled={formLoading}>
        {formLoading ? "Saving..." : "Update Post"}
      </button>
    </form>
  );
};

With Auto-save

import { useForm } from "@refinedev/core";
import { useEffect } from "react";

const PostEdit = () => {
  const { onFinishAutoSave, query, autoSaveProps } = useForm({
    resource: "posts",
    action: "edit",
    id: "1",
    autoSave: {
      enabled: true,
      debounce: 2000,
    },
  });

  const post = query?.data?.data;

  const handleChange = (e) => {
    const formData = new FormData(e.target.form);
    const values = Object.fromEntries(formData.entries());
    onFinishAutoSave(values);
  };

  return (
    <form>
      <div>
        {autoSaveProps.status === "loading" && "Saving..."}
        {autoSaveProps.status === "success" && "Saved!"}
        {autoSaveProps.status === "error" && "Error saving"}
      </div>
      
      <label>
        Title:
        <input 
          name="title" 
          type="text" 
          defaultValue={post?.title} 
          onChange={handleChange}
        />
      </label>
      
      <label>
        Content:
        <textarea 
          name="content" 
          defaultValue={post?.content}
          onChange={handleChange}
        />
      </label>
    </form>
  );
};

Build docs developers (and LLMs) love