# Formisch > The lightweight, schema-first and fully type-safe form library for Angular, Preact, Qwik, React, React Native, Solid, Svelte and Vue. ## Get started (guides) ### Introduction Formisch is a schema-based, headless form library for React Native. It manages form state and validation. It is type-safe, fast by default and its bundle size is small due to its modular design. Try it out in our [playground](/playground/login/)! #### Highlights - Small bundle size starting at 2.5 kB - Schema-based validation with Valibot - Type safety with autocompletion in editor - Open source and fully tested with 100% coverage - It's fast – re-renders only if necessary - Minimal, readable and well thought out API - Supports native `TextInput` fields #### Example Every form starts with the [`useForm`](/react-native/api/useForm.md) hook. It initializes your form's store based on the provided Valibot schema and infers its types. Unlike the DOM frameworks, React Native has no native form element or submit event, so there is no `
` component and submission is triggered explicitly with [`handleSubmit`](/methods/api/handleSubmit.md), for example from a button's `onPress` or a text input's `onSubmitEditing` handler. You can access the state of a field with the [`useField`](/react-native/api/useField.md) hook or the [``](/react-native/api/Field.md) component to connect your `TextInput`. ```tsx import { Field, handleSubmit, useForm } from '@formisch/react-native'; import { Button, Text, TextInput, View } from 'react-native'; import * as v from 'valibot'; const LoginSchema = v.object({ email: v.pipe(v.string(), v.email()), password: v.pipe(v.string(), v.minLength(8)), }); export default function LoginScreen() { const loginForm = useForm({ schema: LoginSchema, }); const submitForm = handleSubmit(loginForm, (output) => console.log(output)); return ( {(field) => ( {field.errors && {field.errors[0]}} )} {(field) => ( {field.errors && {field.errors[0]}} )} ); } ``` The initial input keeps every control defined from its first render. A failed submit moves focus to the first invalid field on its own, so no extra wiring is needed for that. #### Component reference The following snippets assume a form store named `form` and initialized values that match the schema. ##### Text input ```tsx {(field) => ( Email {field.errors?.[0]} )} ``` Use `Textarea` with `TextareaInput` for a longer text field. The wiring is identical. ##### Checkbox ```tsx {(field) => ( { field.props.onFocus(); field.onChange(isChecked); }} > Newsletter )} ``` ##### Select `Select` has no imperative API for opening its menu, so route the element reference to the trigger. `onOpen` is where the field becomes touched, since focusing the control alone does not count as an interaction, and `onClose` reports the blur that ends it: ```tsx {(field) => { // gluestack's `Select` has no imperative open API, so `focus(form, { path })` // is routed to the trigger element. `isFocused` is deliberately omitted: // Formisch treats an element without it as successfully focused instead // of skipping on to the next errored field. const setFieldRef = (element: { focus?: () => void } | null) => field.props.ref(element ? { focus: () => element.focus?.() } : null); return ( Framework {field.errors?.[0]} ); }} ``` ##### Radio group `RadioGroup` reports the new value, so mark the field as touched there: ```tsx {(field) => ( Plan { field.props.onFocus(); field.onChange(value as 'hobby' | 'pro'); }} > Hobby Pro {field.errors?.[0]} )} ``` ##### Slider The slider works with numbers directly, so `field.onChange` can be passed as is. Its thumb is the focusable part, and `onChangeEnd` marks the end of an interaction. There is no native input to register, so `focus` cannot reach this field; do not register the surrounding `View` instead, because React Native cannot verify whether an element took focus and Formisch would treat the failed attempt as successful: ```tsx {(field) => ( Volume: {field.input} field.props.onBlur()} > field.props.onFocus()} /> {field.errors?.[0]} )} ``` #### Library-specific notes When you write a custom control, only expose `isFocused` from its `FieldElement` if it reports the truth. Formisch treats an element without `isFocused` as focused, but one that returns `false` is treated as a failed focus, and it moves on to the next invalid field. The generated components do not typecheck cleanly under `strict` on a fresh scaffold. Those errors are in the generated source rather than in your field wiring, and you can fix them in place since the components live in your project. #### Next steps Read the [input components](/react-native/guides/input-components.md) guide to package repeated wiring into reusable controls, the [controlled fields](/react-native/guides/controlled-fields.md) guide for the underlying pattern, and the [validation](/react-native/guides/validation.md) guide for validation timing. ### React Native Paper [React Native Paper](https://callstack.github.io/react-native-paper/) is a Material Design component library for React Native. This guide targets Paper v5. No adapter package is needed: `TextInput` accepts the whole `field.props` spread, while every other control reports its value through its own callback and marks the field as touched on press. #### Installation Install Formisch, Valibot and React Native Paper, then wrap your app in `PaperProvider` as described in the [Paper getting started guide](https://callstack.github.io/react-native-paper/docs/guides/getting-started): ```bash npx expo install @formisch/react-native valibot react-native-paper react-native-safe-area-context ``` The slider section below additionally uses `@react-native-community/slider`, which is a separate package rather than part of Paper: ```bash npx expo install @react-native-community/slider ``` Import everything from `@formisch/react-native`. That package bundles the core and the methods, so importing from `@formisch/methods/react-native` alongside it would load a second copy of Formisch's reactive state and break updates in field arrays. #### Wiring patterns React Native has no DOM, so `field.props` is only `{ ref, onFocus, onBlur, onChangeText }`. There is no `name` and no `autoFocus` to forward: - On a text input, spread `field.props` and pass `field.input` as the value. - On any other control, pass `field.input` as the value, send the control's callback to `field.onChange`, and call `field.props.onFocus()` in the press handler. That last point is the important one. A press does not raise a focus event in React Native, so without calling `field.props.onFocus()` yourself the field never becomes touched and touch validation never runs. ##### Spreading field.props Paper's `TextInput` forwards the spread to the underlying native input: ```tsx {(field) => ( )} ``` Paper's `error` prop switches the input to its error styling. React Native has no equivalent of the `aria-invalid` attribute, so the visible `HelperText` below the input is what conveys the error. ##### Controlled components `Checkbox.Item` accepts no `ref`, so there is no element to register and [`focus`](/methods/api/focus.md) cannot reach this field. Do not register a wrapping `View` to work around it: React Native cannot verify whether an element took focus, so Formisch trusts any registered element that cannot report its focus state and stops there, which makes a failed focus look successful. Mark the field as touched in the press handler instead: ```tsx {(field) => ( { // Press does not trigger focus, so mark the field as touched here field.props.onFocus(); field.onChange(!field.input); }} /> )} ``` Note that the programmatic setter is `field.onChange(value)`. In React Native, `field.props` carries only `onChangeText`, which is meant for text input. #### Displaying errors Formisch exposes errors as `[string, ...string[]] | null`. Paper's `HelperText` renders the message and unmounts it when the field becomes valid again: ```tsx {(field) => ( {field.errors?.[0]} )} ``` Paper does not associate the helper text with the input, so there is no equivalent of `aria-errormessage` here. By default, Formisch validates on submit and revalidates on input. The [validation guide](/react-native/guides/validation.md) explains how to change that timing. #### Login form example React Native has no `Form` component. Wrap the fields in a `View` and pass `handleSubmit` straight to a button, since it returns a function that takes no arguments: ```tsx import { Field, handleSubmit, useForm } from '@formisch/react-native'; import { View } from 'react-native'; import { Button, Checkbox, HelperText, TextInput } from 'react-native-paper'; import * as v from 'valibot'; const LoginSchema = v.object({ email: v.pipe( v.string(), v.nonEmpty('Please enter your email.'), v.email('The email address is badly formatted.') ), password: v.pipe( v.string(), v.nonEmpty('Please enter your password.'), v.minLength(8, 'Your password must have 8 characters or more.') ), rememberMe: v.optional(v.boolean(), false), }); export function LoginForm() { const loginForm = useForm({ schema: LoginSchema, initialInput: { email: '', password: '', rememberMe: false }, }); const submitForm = handleSubmit(loginForm, (output) => { console.log(output); }); return ( {(field) => ( {field.errors?.[0]} )} {(field) => ( {field.errors?.[0]} )} {(field) => ( // `Checkbox.Item` accepts no ref, so `focus` cannot reach this field { // Press does not trigger focus, so mark the field as touched here field.props.onFocus(); field.onChange(!field.input); }} /> )} ); } ``` The initial input keeps every control defined from its first render. A failed submit moves focus to the first invalid field on its own, so no extra wiring is needed for that. #### Component reference The following snippets assume a form store named `form` and initialized values that match the schema. ##### Text input ```tsx {(field) => ( {field.errors?.[0]} )} ``` Add `multiline` for a longer text field. The wiring stays the same. ##### Checkbox ```tsx {(field) => ( { field.props.onFocus(); field.onChange(!field.input); }} /> )} ``` ##### Select Paper has no select component. Build one from `Menu` with a pressable anchor, and expose a `FieldElement` through `useImperativeHandle` so `focus` can open it. The component below reads `ref` from its props, which requires React 19; on React 18 wrap it in `forwardRef` instead: ```tsx function MenuSelect({ ref, onFocus, onBlur, label, value, options, onValueChange, }: Pick & { label: string; value: TValue | undefined; options: readonly { label: string; value: TValue }[]; onValueChange: (value: TValue) => void; }) { const [visible, setVisible] = useState(false); // The menu closing ends the interaction, so report it as a blur const close = () => { setVisible(false); onBlur(); }; // Expose a `FieldElement` so `focus(form, { path })` can reach this control. // `isFocused` is deliberately omitted: Formisch treats an element without it // as successfully focused, instead of skipping to the next errored field. useImperativeHandle(ref, () => ({ focus: () => setVisible(true), blur: close, })); return ( { onFocus(); setVisible(true); }} > {options.find((option) => option.value === value)?.label ?? label} } > {options.map((option) => ( { onValueChange(option.value); close(); }} /> ))} ); } ``` Making the component generic over `TValue` keeps it assignable to `field.onChange`, which expects the schema's union rather than a plain string: ```tsx {(field) => ( )} ``` For a small set of options, `SegmentedButtons` is a simpler alternative. ##### Radio group `RadioButton.Group` reports the new value, so mark the field as touched there. The group needs its own accessible name, since the individual options only announce their own labels: ```tsx {(field) => ( { field.props.onFocus(); field.onChange(value as 'hobby' | 'pro'); }} > {field.errors?.[0]} )} ``` ##### Slider Paper has no slider. The community package `@react-native-community/slider` works with numbers directly, so `field.onChange` can be passed as is, and its drag events map onto the focus and blur handlers: ```tsx {(field) => ( Volume: {field.input} field.props.onFocus()} onSlidingComplete={() => field.props.onBlur()} onValueChange={field.onChange} /> )} ``` #### Library-specific notes When you write a custom control, only expose `isFocused` from its `FieldElement` if it reports the truth. Formisch treats an element without `isFocused` as focused, but one that returns `false` is treated as a failed focus, and it moves on to the next invalid field. #### Next steps Read the [input components](/react-native/guides/input-components.md) guide to package repeated wiring into reusable controls, the [controlled fields](/react-native/guides/controlled-fields.md) guide for the underlying pattern, and the [validation](/react-native/guides/validation.md) guide for validation timing. ## Hooks (api) ### useForm Creates a reactive form store from a form configuration. The form store manages form state and provides reactive properties. ```ts const form = useForm(config); ``` #### Generics - `TSchema` `extends FormSchema` #### Parameters - `config` `FormConfig` ##### Explanation `useForm` creates a reactive form store that manages form state using the provided Valibot schema. Validation runs when the form is submitted by default, after which fields revalidate on every input, but this can be customized with the `validate` and `revalidate` options. Since React Native has no native form element or submit event, there is no `` component. Instead, submission is triggered explicitly with the [`handleSubmit`](/methods/api/handleSubmit.md) method, for example from a button's `onPress` or a text input's `onSubmitEditing` handler. #### Returns - `form` `FormStore` #### Examples The following examples show how `useForm` can be used. ##### Login screen ```tsx import { Field, handleSubmit, useForm } from '@formisch/react-native'; import { Button, Text, TextInput, View } from 'react-native'; import * as v from 'valibot'; const LoginSchema = v.object({ email: v.pipe(v.string(), v.email()), password: v.pipe(v.string(), v.minLength(8)), }); export default function LoginScreen() { const loginForm = useForm({ schema: LoginSchema, }); const submitForm = handleSubmit(loginForm, (output) => console.log(output)); return ( {(field) => ( {field.errors && {field.errors[0]}} )} {(field) => ( {field.errors && {field.errors[0]}} )}