Skip to main content
Version: v0.30.x

Forms

Every picker is a controlled component, so it plugs into any form library. In a form you usually want a field that opens the picker: SelectPicker and the picker fields (DatePickerField, TimePickerField, TimeRangePickerField, DateTimePickerField, WheelPickerField, DurationPickerField, MonthPickerField, ColorPickerField, EmojiPickerField) render one, and report the value through onChange (select) or onConfirm (fields). Their onClose is the natural place for onBlur.

Every field, plus SelectPicker, CountryPicker, TimeZonePicker, PhoneField and AmountField, shows your form's errors: pass the message as error and the field turns red with the message under it. helperText is the line under the field while there's no error, and required adds an asterisk to the label. The fields don't validate anything themselves; your form library decides what's invalid and when to show it.

React Hook Form​

import { useForm, Controller } from 'react-hook-form';
import { View, Button } from 'react-native';
import { SelectPicker, DatePickerField, TimePickerField } from '@elik56a/react-native-pickers';
import type { DateRange } from '@elik56a/react-native-pickers';

type FormValues = {
country: string | null;
tags: string[];
date: Date | null;
time: Date;
stay: DateRange;
};

export function BookingForm() {
const { control, handleSubmit } = useForm<FormValues>({
defaultValues: { country: null, tags: [], date: null, time: new Date(), stay: { start: null, end: null } },
});

return (
<View style={{ gap: 16 }}>
<Controller
control={control}
name="country"
rules={{ required: 'Pick a country' }}
render={({ field, fieldState }) => (
<SelectPicker
label="Country"
required
error={fieldState.error?.message}
options={countries}
value={field.value}
onChange={field.onChange}
onClose={field.onBlur}
/>
)}
/>

<Controller
control={control}
name="tags"
rules={{ validate: (v) => v.length > 0 || 'Pick at least one tag' }}
render={({ field, fieldState }) => (
<SelectPicker multiple label="Tags" error={fieldState.error?.message} options={tags} value={field.value} onChange={field.onChange} />
)}
/>

<Controller
control={control}
name="stay"
rules={{ validate: (r) => (!!r.start && !!r.end) || 'Choose check-in and check-out' }}
render={({ field, fieldState }) => (
<DatePickerField
label="Stay"
placeholder="Add dates"
required
helperText="Free cancellation until the day before"
error={fieldState.error?.message}
mode="range"
value={field.value}
onConfirm={field.onChange}
onClose={field.onBlur}
minDate={new Date()}
/>
)}
/>

<Controller
control={control}
name="time"
render={({ field }) => <TimePickerField label="Arrival" value={field.value} onConfirm={field.onChange} onClose={field.onBlur} />}
/>

<Button title="Submit" onPress={handleSubmit(console.log)} />
</View>
);
}

field.onChange receives the first argument, which is the value, so extra arguments such as the option from SelectPicker are ignored. fieldState.error is set after a submit, or after a blur in onBlur mode; since onClose calls field.onBlur, closing a picker counts as leaving the field.

Phone numbers​

PhoneField calls onChange with the E.164 value and details, whose valid says whether the number has the country's length. field.onChange keeps only the value, so check it again in validate:

<Controller
control={control}
name="phone"
rules={{ validate: (v) => isValidPhoneNumber(v) || 'Enter a full number' }}
render={({ field, fieldState }) => (
<PhoneField label="Mobile" required error={fieldState.error?.message} value={field.value} onChange={field.onChange} />
)}
/>

isValidPhoneNumber here is your own check on the E.164 value: parsePhone and isValidPhone from the library build it in two lines:

import { isValidPhone, parsePhone } from '@elik56a/react-native-pickers';

const isValidPhoneNumber = (e164: string) => {
const { country, nationalNumber } = parsePhone(e164);
return !!country && isValidPhone(nationalNumber, country);
};

Options from your API​

A field whose options come from a server takes loadOptions instead of a fixed list. It searches as the user types, with a loader, and the form only ever sees the picked value:

<Controller
control={control}
name="assignee"
rules={{ required: 'Assign someone' }}
render={({ field }) => (
<SelectPicker
label="Assignee"
placeholder="Search people"
loadOptions={(query) => api.searchUsers(query).then((users) => users.map((u) => ({ label: u.name, value: u.id })))}
value={field.value}
onChange={field.onChange}
onClose={field.onBlur}
/>
)}
/>

The field keeps showing the picked person's name after later searches stop returning them. For a full form with server search, see the flight search form recipe.

Optional values​

A field with clearable shows a × button while it has a value. Reset the form value in onClear:

<Controller
control={control}
name="date"
render={({ field }) => (
<DatePickerField label="Due date" value={field.value} onConfirm={field.onChange} clearable onClear={() => field.onChange(null)} />
)}
/>

Inline pickers​

The inline pickers (DatePicker, TimePicker, WheelPicker…) take value + onChange the same way, for a picker that's always on screen:

<Controller control={control} name="time" render={({ field }) => <TimePicker value={field.value} onChange={field.onChange} />} />

Formik​

<Formik initialValues={{ birthday: new Date(2000, 0, 1), size: 'M' }} onSubmit={save}>
{({ values, setFieldValue }) => (
<>
<DatePickerField label="Birthday" display="wheel" value={values.birthday} onConfirm={(d) => setFieldValue('birthday', d)} />
<WheelPickerField label="Size" items={['S', 'M', 'L']} value={values.size} onConfirm={(v) => setFieldValue('size', v)} />
</>
)}
</Formik>

Sending dates to an API​

Pickers work with local Date objects. Serialize them for your API deliberately:

import { toDateKey } from '@elik56a/react-native-pickers';

// A calendar day (no time zone): '2026-10-14'
const day = toDateKey(date);

// An instant: '2026-10-14T07:30:00.000Z'
const instant = time.toISOString();

// Parse a 'YYYY-MM-DD' back to a local date (don't use new Date('2026-10-14'), which parses as UTC)
const [y, m, d] = '2026-10-14'.split('-').map(Number);
const local = new Date(y, m - 1, d);
Day keys vs ISO strings

date.toISOString().slice(0, 10) converts to UTC first, and can give yesterday or tomorrow depending on the user's time zone. Use toDateKey for calendar days.

Zod schema​

import { z } from 'zod';

const schema = z.object({
stay: z
.object({ start: z.date().nullable(), end: z.date().nullable() })
.refine((r) => r.start && r.end, 'Choose both dates'),
country: z.string({ required_error: 'Pick a country' }),
});