I’m working on a React form using React Hook Form and Zod. Most fields are primitive but there is one that requires an additional nested component. This field is an array and the nested component provides some operations to add and remove elements.
For the sake of simplicity let’s say there is one field foo
that is a primitive string field and the field bars
which is an array of strings.
The following shows the whole form component ( playground for reproduction )
import { zodResolver } from '@hookform/resolvers/zod';
import {
type SubmitHandler,
useForm,
UseFormRegister,
UseFormSetValue,
FieldErrors,
} from 'react-hook-form';
import { z } from 'zod';
function App() {
const formFieldsSchema = z.object({
foo: z.string().min(1),
bars: z.array(z.string().min(1)).min(1),
});
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setValue,
getValues,
} = useForm<z.infer<typeof formFieldsSchema>>({
resolver: zodResolver(formFieldsSchema),
defaultValues: {
foo: '',
bars: [],
},
});
const onSubmit: SubmitHandler<z.infer<typeof formFieldsSchema>> = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>Foo:</div>
<input type="text" {...register('foo')} disabled={isSubmitting} />
{errors.foo && <div>{errors.foo.message}</div>}
<BarsContainer
bars={getValues('bars')}
register={register}
setValue={setValue}
errors={errors}
isSubmitting={isSubmitting}
/>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
);
}
interface BarsContainerProps {
bars: string[];
register: UseFormRegister<{
foo: string;
bars: string[];
}>;
setValue: UseFormSetValue<{
foo: string;
bars: string[];
}>;
errors: FieldErrors<{
foo: string;
bars: string[];
}>;
isSubmitting: boolean;
}
function BarsContainer({
bars,
register,
setValue,
errors,
isSubmitting,
}: BarsContainerProps) {
const addBar = () => {
setValue('bars', [...bars, '']);
};
const changeBar = (newBar: string, index: number) => {
setValue(
'bars',
bars.map((currentBar, currentBarIndex) => {
if (currentBarIndex !== index) {
return currentBar;
}
return newBar;
})
);
};
const removeBar = (index: number) => {
setValue(
'bars',
bars.filter((_, i) => i !== index)
);
};
return (
<>
<div>Bars</div>
<button type="button" disabled={isSubmitting} onClick={addBar}>
Add bar
</button>
{bars.map((bar, index) => (
<div key={index}>
<input
type="text"
{...register(`bars.${index}` as const)}
disabled={isSubmitting}
value={bar}
onChange={(e) => changeBar(e.target.value, index)}
/>
<button
type="button"
disabled={isSubmitting}
onClick={() => removeBar(index)}
>
Remove
</button>
</div>
))}
{errors.bars && <div>{errors.bars?.message}</div>}
</>
);
}
The field foo
works as expected, but the field bars
seems to be broken.
- When I want to add a new array element nothing happens. But the element will be added when clicking on the submit button.
- When I want to change the value of an array element nothing happens, the text field remains empty.
How can I fix this code?
My first workaround
I’m using a useState
hook for the bars
field. The BarsContainer
keeps working with this hook. In the App
I’m using a useEffect
hook that calls setValue('bars', bars);
whenever bars
changes.
Playground
But not the ideal solution..
1