form.tsx component which is basically a shadcn multi-step form with zod & react-hook-form.
but, the state does not change and not update, maybe due to it being async but I don’t really know why. The state should be updated according to the numberOfDays, and then I can know how many more forms to render, update the steps based on the number of days the user has chosen
So, this is the component:
export default function StepperForm() {
const { formData, setFormData } = useStepper();
const [steps, setSteps] = useState([{ label: "Plan Details" }]);
useEffect(() => {
setSteps((prevSteps) => [
...prevSteps,
...(formData.numberOfDays
? Array.from({ length: formData.numberOfDays }, (_, i) => ({
label: `אימון ${i + 1}`,
}))
: []),
]);
}, [formData.numberOfDays]);
const handleFormSubmit = (data: any) => {
setFormData({
...formData,
...data,
numberOfDays: data.numberOfDays || 1,
});
};
return (
<div className="flex w-full flex-col gap-4">
<Stepper variant="circle-alt" initialStep={0} steps={steps}>
{steps.map((stepProps, index) => {
if (index === 0) {
return (
<Step key={stepProps.label} {...stepProps}>
<FirstStepForm onSubmit={handleFormSubmit} />
</Step>
);
}
return (
<Step key={stepProps.label} {...stepProps}>
<SecondStepForm />
</Step>
);
})}
<MyStepperFooter />
</Stepper>
</div>
);
}
const FirstFormSchema = z.object({
title: z.string().min(2),
level: z.enum(["Beginner", "Intermediate", "Advanced"]),
numberOfDays: z
.string()
.refine((val) => val === "" || !isNaN(parseInt(val)), {
message: "Please select a number of days",
})
.transform((val) => (val === "" ? undefined : parseInt(val, 10))),
});
function FirstStepForm({ onSubmit }: { onSubmit: (data: any) => void }) {
const { nextStep } = useStepper();
const form = useForm<z.infer<typeof FirstFormSchema>>({
resolver: zodResolver(FirstFormSchema),
});
function handleSubmit(data: z.infer<typeof FirstFormSchema>) {
onSubmit(data);
nextStep();
toast.success("First step submitted!");
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder="Enter your title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="בחר רמת האימון" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Beginner">Beginner</SelectItem>
<SelectItem value="Intermediate">Intermediate</SelectItem>
<SelectItem value="Advanced">Advanced</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="numberOfDays"
render={({ field }) => (
<FormItem>
<Select
onValueChange={field.onChange}
value={field.value?.toString() || undefined}
>
<FormControl>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="בחר מספר אימונים בשבוע" />
</SelectTrigger>
</FormControl>
<SelectContent>
{["1", "2", "3", "4", "5", "6", "7"].map((day) => (
<SelectItem key={day} value={day}>
{day}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<StepperFormActions />
</form>
</Form>
);
}
but, when user chooses the days and updates, the useEffect does not get triggered and not updating steps at all.
formData and setFormData are react-context, which is in a component called use-stepper.ts
import * as React from "react";
import { StepperContext } from "./context";
function usePrevious<T>(value: T): T | undefined {
const ref = React.useRef<T>();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export function useStepper() {
const context = React.useContext(StepperContext);
if (context === undefined) {
throw new Error("useStepper must be used within a StepperProvider");
}
const { children, className, formData, ...rest } = context;
const isFirstStep = context.activeStep === 0;
const isLastStep = context.activeStep === context.steps.length - 1;
const hasCompletedAllSteps = context.activeStep === context.steps.length;
const previousActiveStep = usePrevious(context.activeStep);
const currentStep = context.steps[context.activeStep];
const isOptionalStep = !!currentStep?.optional;
const isDisabledStep = context.activeStep === 0;
return {
...rest,
isLastStep,
hasCompletedAllSteps,
isOptionalStep,
isDisabledStep,
currentStep,
previousActiveStep,
isFirstStep,
formData,
};
}
and this is the stepper-context.tsx
import * as React from "react";
import type { StepperProps } from "./types";
interface FormData {
title: string;
level: "Beginner" | "Intermediate" | "Advanced";
numberOfDays: number | undefined;
}
interface StepperContextValue extends StepperProps {
clickable?: boolean;
isError?: boolean;
isLoading?: boolean;
isVertical?: boolean;
stepCount?: number;
expandVerticalSteps?: boolean;
activeStep: number;
initialStep: number;
formData: FormData;
setFormData: React.Dispatch<React.SetStateAction<FormData>>;
nextStep: () => void;
prevStep: () => void;
resetSteps: () => void;
setStep: (step: number) => void;
}
type StepperContextProviderProps = {
value: Omit<
StepperContextValue,
| "activeStep"
| "formData"
| "setFormData"
| "nextStep"
| "prevStep"
| "resetSteps"
| "setStep"
>;
children: React.ReactNode;
};
const defaultFormData: FormData = {
title: "",
level: "Beginner",
numberOfDays: undefined,
};
const StepperContext = React.createContext<StepperContextValue>({
steps: [],
activeStep: 0,
initialStep: 0,
formData: defaultFormData,
setFormData: () => {},
nextStep: () => {},
prevStep: () => {},
resetSteps: () => {},
setStep: () => {},
});
const StepperProvider: React.FC<StepperContextProviderProps> = ({
value,
children,
}) => {
const isError = value.state === "error";
const isLoading = value.state === "loading";
const [activeStep, setActiveStep] = React.useState(value.initialStep);
const [formData, setFormData] = React.useState<FormData>(defaultFormData);
const steps = React.useMemo(
() => [
{ label: "פרטי תוכנית", description: "הגדרת פרטי התוכנית" },
...(formData.numberOfDays
? Array.from({ length: formData.numberOfDays }, (_, i) => ({
label: `אימון ${i + 1}`,
description: `הגדרת אימון ${i + 1}`,
}))
: []),
],
[formData.numberOfDays],
);
const nextStep = React.useCallback(() => {
setActiveStep((prev) => prev + 1);
}, []);
const prevStep = React.useCallback(() => {
setActiveStep((prev) => prev - 1);
}, []);
const resetSteps = React.useCallback(() => {
setActiveStep(value.initialStep);
}, [value.initialStep]);
const setStep = React.useCallback((step: number) => {
setActiveStep(step);
}, []);
const contextValue = React.useMemo(
() => ({
...value,
steps,
isError,
isLoading,
activeStep,
formData,
setFormData,
nextStep,
prevStep,
resetSteps,
setStep,
}),
[
value,
steps,
isError,
isLoading,
activeStep,
formData,
nextStep,
prevStep,
resetSteps,
setStep,
],
);
return (
<StepperContext.Provider value={contextValue}>
{children}
</StepperContext.Provider>
);
};
export { StepperContext, StepperProvider };