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.

INFORMATION

useStepsForm hook is extended from useForm from the @pankod/refine-react-hook-form package. This means you can use all the features of useForm.

Basic Usage​

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

Here is the final result of the form: We will explain the code in following sections.

localhost:3000/posts/create
Live previews only work with the latest documentation.
import { useSelect, HttpError } from "@pankod/refine-core";
import { useStepsForm } from "@pankod/refine-react-hook-form";

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

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

const { options } = useSelect<ICategory, HttpError>({
resource: "categories",
});

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 }}>
<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>
<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>
);
};

interface ICategory {
id: number;
title: string;
}

interface IPost {
id: number;
title: string;
content: string;
status: "published" | "draft" | "rejected";
category: {
id: ICategory["id"];
};
}

In this example we're going to build a Post "create" form. We also added a relational category field to expand our example.

To split your <input/> components under a <form/> component, first import and use useStepsForm hook in your page:

import { HttpError, useSelect } from "@pankod/refine-core";
import { useStepsForm } from "@pankod/refine-react-hook-form";

const PostCreate = () => {
const {
refineCore: { onFinish, formLoading },
register,
handleSubmit,
formState: { errors },
steps: { currentStep, gotoStep },
} = useStepsForm<IPost, HttpError, IPost>();

return <div>...</div>;
};

interface ICategory {
id: number;
title: string;
}

interface IPost {
id: number;
title: string;
content: string;
status: "published" | "draft" | "rejected";
category: {
id: ICategory["id"];
};
}

useStepsForm is generic over the type form data to help you type check your code.

This hook returns a set of useful values to render steps form. Given current value, you should have a way to render your form items conditionally with this index value.

Here, we're going to use a switch statement to render the form items based on the currentStep.

import { HttpError, useSelect } from "@pankod/refine-core";
import { useStepsForm } from "@pankod/refine-react-hook-form";

const PostCreate = () => {
const {
refineCore: { onFinish, formLoading },
register,
handleSubmit,
formState: { errors },
steps: { currentStep, gotoStep },
} = useStepsForm<IPost, HttpError, IPost>();

const { options } = useSelect<ICategory, HttpError>({
resource: "categories",
});

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>}
</>
);
}
};

return <div>...</div>;
};

interface ICategory {
id: number;
title: string;
}

interface IPost {
id: number;
title: string;
content: string;
status: "published" | "draft" | "rejected";
category: {
id: ICategory["id"];
};
}
TIP

Since category is a relational data, we use useSelect to fetch its data.

Refer to useSelect documentation for detailed usage. →

Now, we can use renderFormByStep function to render the form items based on the currentStep and gotoStep function to navigate between steps.

import { HttpError, useSelect } from "@pankod/refine-core";
import { useStepsForm } from "@pankod/refine-react-hook-form";

const PostCreate = () => {
const {
refineCore: { onFinish, formLoading },
register,
handleSubmit,
formState: { errors },
steps: { currentStep, gotoStep },
} = useStepsForm<IPost, HttpError, IPost>();

const { options } = useSelect<ICategory, HttpError>({
resource: "categories",
});

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 }}>
<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>
<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>
);
};

interface ICategory {
id: number;
title: string;
}

interface IPost {
id: number;
title: string;
content: string;
status: "published" | "draft" | "rejected";
category: {
id: ICategory["id"];
};
}

Properties​

refineCoreProps​

All useForm properties also available in useStepsForm. You can find descriptions on useForm docs.

const stepsForm = useStepsForm({
refineCoreProps: {
action: "edit",
resource: "posts",
id: "1",
},
});

stepsProps​

The props needed by the manage state steps.

defaultStep​

Default: 0

Sets the default starting step number. Counting starts from 0.

const stepsForm = useStepsForm({
stepsProps: {
defaultStep: 0,
},
});

isBackValidate​

Default: false

When is true, validates a form fields when the user navigates to a previous step.

const stepsForm = useStepsForm({
stepsProps: {
isBackValidate: true,
},
});

Return Values​

TIP

All useForm return values also available in useStepsForm. You can find descriptions on useForm docs.

steps​

The return values needed by the manage state steps.

currenStep​

Current step, counting from 0.

gotoStep​

Is a function that allows you to programmatically change the current step of a form. It takes in one argument, step, which is a number representing the index of the step you want to navigate to.

API Reference​

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.

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

Example​

Run on your local
npm create refine-app@latest -- --example form-react-hook-form-use-steps-form