Skip to content

react-hook-form

raqam works naturally with react-hook-form via the Controller component. The key is: raqam is value/onChange controlled, and Controller supplies exactly those.

Terminal window
npm install react-hook-form
import { useForm, Controller } from "react-hook-form";
import { NumberField } from "raqam";
type FormValues = {
price: number | null;
};
export function PriceForm() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({ defaultValues: { price: null } });
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="price"
control={control}
rules={{
required: "Price is required",
min: { value: 0.01, message: "Must be at least $0.01" },
max: { value: 999999, message: "Cannot exceed $999,999" },
}}
render={({ field }) => (
<NumberField.Root
locale="en-US"
formatOptions={{ style: "currency", currency: "USD" }}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
>
<NumberField.Label>Price</NumberField.Label>
<NumberField.Group>
<NumberField.Decrement></NumberField.Decrement>
<NumberField.Input />
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Group>
{errors.price && (
<p style={{ color: "red", fontSize: 12 }}>
{errors.price.message}
</p>
)}
</NumberField.Root>
)}
/>
<button type="submit">Submit</button>
</form>
);
}

For simple validation, use raqam’s validate prop directly alongside react-hook-form — this updates aria-invalid and renders NumberField.ErrorMessage:

<Controller
name="amount"
control={control}
rules={{ required: true }}
render={({ field, fieldState }) => (
<NumberField.Root
locale="en-US"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
validate={(v) => {
if (v === null) return "Required";
if (v < 1) return "Min $1";
return true;
}}
>
<NumberField.Label>Amount</NumberField.Label>
<NumberField.Input />
<NumberField.ErrorMessage />
</NumberField.Root>
)}
/>
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
price: z
.number({ required_error: "Price is required" })
.min(0.01, "Must be positive")
.max(999999, "Too large"),
});
export function PriceForm() {
const { control, handleSubmit } = useForm({
resolver: zodResolver(schema),
defaultValues: { price: 0 },
});
return (
<form onSubmit={handleSubmit(console.log)}>
<Controller
name="price"
control={control}
render={({ field, fieldState }) => (
<NumberField.Root
locale="en-US"
formatOptions={{ style: "currency", currency: "USD" }}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
>
<NumberField.Label>Price</NumberField.Label>
<NumberField.Input />
{fieldState.error && (
<p style={{ color: "red" }}>{fieldState.error.message}</p>
)}
</NumberField.Root>
)}
/>
<button type="submit">Save</button>
</form>
);
}
type InvoiceForm = {
subtotal: number | null;
tax: number | null;
discount: number | null;
};
const currencyField = (name: keyof InvoiceForm, label: string) => (
<Controller
name={name}
control={control}
render={({ field }) => (
<NumberField.Root
locale="en-US"
formatOptions={{ style: "currency", currency: "USD" }}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
minValue={0}
>
<NumberField.Label>{label}</NumberField.Label>
<NumberField.Input />
</NumberField.Root>
)}
/>
);