Skip to main content
Version: 3.xx.xx

useStepsForm

useStepsForm allows you to manage a form with multiple steps. It provides features such as which step is currently active, the ability to go to a specific step and validation when changing steps etc.

info

useStepsForm hook is extended from useForm from the @pankod/refine-react-hook-form package.

Usage

We'll show two examples, one for creating and one for editing a post. Let's see how useStepsForm is used in both.

Let's create our <PostList> to redirect to create and edit pages.

PostList

In this component we will use useNavigation to redirect to the <PostCreate> and <PostEdit> components.

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

import { ICategory, IPost } from "interfaces";

export const PostList: React.FC = () => {
const { tableQueryResult } = useTable<IPost>({
initialSorter: [
{
field: "id",
order: "desc",
},
],
});
const { edit, create } = useNavigation();

const categoryIds =
tableQueryResult?.data?.data.map((item) => item.category.id) ?? [];
const { data, isLoading } = useMany<ICategory>({
resource: "categories",
ids: categoryIds,
queryOptions: {
enabled: categoryIds.length > 0,
},
});

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

Create Form

In this component you can see how useStepsForm is used to manage the steps and form.

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

const stepTitles = ["Title", "Status", "Content"];

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

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

// Where buttons are shown and where the form is submitted
const renderFormByStep = (step: number) => {
switch (step) {
case 0:
return (
<>
<label>Title: </label>
<input
{...register("title", {
required: "This field is required",
})}
/>
{errors.title && <span>{errors.title.message}</span>}
</>
);
case 1:
return (
<>
<label>Status: </label>
<select {...register("status")}>
<option value="published">published</option>
<option value="draft">draft</option>
<option value="rejected">rejected</option>
</select>
</>
);
case 2:
return (
<>
<label>Category: </label>
<select
{...register("category.id", {
required: "This field is required",
})}
>
{options?.map((category) => (
<option
key={category.value}
value={category.value}
>
{category.label}
</option>
))}
</select>
{errors.category && (
<span>{errors.category.message}</span>
)}
<br />
<br />
<label>Content: </label>
<textarea
{...register("content", {
required: "This field is required",
})}
rows={10}
cols={50}
/>
{errors.content && (
<span>{errors.content.message}</span>
)}
</>
);
}
};

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

return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
// Where step titles are shown
<div style={{ display: "flex", gap: 36 }}>
{stepTitles.map((title, index) => (
<button
key={index}
onClick={() => gotoStep(index)}
style={{
backgroundColor:
currentStep === index ? "lightgray" : "initial",
}}
>
{index + 1} - {title}
</button>
))}
</div>
<form autoComplete="off">{renderFormByStep(currentStep)}</form>
// Where buttons are shown
<div style={{ display: "flex", gap: 8 }}>
{currentStep > 0 && (
<button
onClick={() => {
gotoStep(currentStep - 1);
}}
>
Previous
</button>
)}
{currentStep < stepTitles.length - 1 && (
<button
onClick={() => {
gotoStep(currentStep + 1);
}}
>
Next
</button>
)}
{currentStep === stepTitles.length - 1 && (
<button onClick={handleSubmit(onFinish)}>Save</button>
)}
</div>
</div>
);
};
Create Form

Edit Page

Magic, <PostCreate> and <PostEdit> pages are almost the same. So how are the form's default values set? useStepsForm does this with te id parameter it reads from the URL and fetches the data from the server.

You can change the id as you want with the setId that comes out of refineCore.

Another part that is different from <PostCreate> and <PostEdit> is the defaultValue value passed to the useSelect hook and the <select> element.

Refer to the useSelect documentation for detailed information. β†’

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

const stepTitles = ["Title", "Status", "Category and content"];

export const PostEdit: React.FC = () => {
const {
refineCore: { onFinish, formLoading, queryResult },
register,
handleSubmit,
formState: { errors },
steps: { currentStep, gotoStep },
} = useStepsForm();

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

// It handles which form elements render at which step.
const renderFormByStep = (step: number) => {
switch (step) {
case 0:
return (
<>
<label>Title: </label>
<input
{...register("title", {
required: "This field is required",
})}
/>
{errors.title && <span>{errors.title.message}</span>}
</>
);
case 1:
return (
<>
<label>Status: </label>
<select {...register("status")}>
<option value="published">published</option>
<option value="draft">draft</option>
<option value="rejected">rejected</option>
</select>
</>
);
case 2:
return (
<>
<label>Category: </label>
<select
{...register("category.id", {
required: "This field is required",
})}
defaultValue={queryResult?.data?.data.category.id}
>
{options?.map((category) => (
<option
key={category.value}
value={category.value}
>
{category.label}
</option>
))}
</select>
{errors.category && (
<span>{errors.category.message}</span>
)}
<br />
<br />
<label>Content: </label>
<textarea
{...register("content", {
required: "This field is required",
})}
rows={10}
cols={50}
/>
{errors.content && (
<span>{errors.content.message}</span>
)}
</>
);
}
};

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

return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
// Where step titles are shown
<div style={{ display: "flex", gap: 36 }}>
{stepTitles.map((title, index) => (
<button
key={index}
onClick={() => gotoStep(index)}
style={{
backgroundColor:
currentStep === index ? "lightgray" : "initial",
}}
>
{index + 1} - {title}
</button>
))}
</div>
<form autoComplete="off">{renderFormByStep(currentStep)}</form>
// Where buttons are shown
<div style={{ display: "flex", gap: 8 }}>
{currentStep > 0 && (
<button
onClick={() => {
gotoStep(currentStep - 1);
}}
>
Previous
</button>
)}
{currentStep < stepTitles.length - 1 && (
<button
onClick={() => {
gotoStep(currentStep + 1);
}}
>
Next
</button>
)}
{currentStep === stepTitles.length - 1 && (
<button onClick={handleSubmit(onFinish)}>Save</button>
)}
</div>
</div>
);
};
Edit Form

API Reference

Properties

PropertyDescriptionType
stepsPropsConfiguration object for the stepsStepsPropsType
refineCorePropsConfiguration object for the core of the useFormUseFormProps
React Hook Form PropertiesSee React Hook Form documentation

PropertyDescriptionTypeDefault
defaultStepAllows you to set the initial stepnumber0
isBackValidateWhether to validation the current step when going back.booleanfalse

Return values

PropertyDescriptionType
stepsRelevant state and method to control the stepsStepsReturnValues
refineCoreThe return values of the useForm in the coreUseFormReturnValues
React Hook Form Return ValuesSee React Hook Form documentation

PropertyDescriptionType
currentStepCurrent stepboolean
gotoStepAllows you to go to a specific step.(step: number) => void

Live StackBlitz Example