Input components
To make your code more readable, we recommend that you develop your own input components if you are not using a prebuilt UI library. There you can encapsulate logic to display error messages, for example.
If you're already a bit more experienced, you can use the input components we developed for our playground as a starting point. You can find the code in our GitHub repository here.
Why input components?
Currently, your fields might look something like this:
<Field of={loginForm} path={['email']}>
{(field) => (
<View>
<Text>Email</Text>
<TextInput
{...field.props}
value={field.input}
autoCapitalize="none"
keyboardType="email-address"
/>
{field.errors && <Text>{field.errors[0]}</Text>}
</View>
)}
</Field>If styling and a few more functionalities are added here, the code quickly becomes confusing. In addition, you have to rewrite the same code for almost every form field.
Our goal is to develop a TextInput component so that the code ends up looking like this:
<Field of={loginForm} path={['email']}>
{(field) => (
<TextInput
{...field.props}
type="email"
label="Email"
input={field.input}
errors={field.errors}
required
/>
)}
</Field>Create an input component
In the first step, you create a new file for the TextInput component and, if you use TypeScript, define its properties. By extending FieldElementProps, your component accepts the props of the field store, which include the event handlers and the ref callback.
import type { FieldElementProps } from '@formisch/react-native';
interface TextInputProps extends FieldElementProps {
type: 'text' | 'email' | 'tel' | 'password' | 'url';
label?: string;
placeholder?: string;
input: string | undefined;
errors: [string, ...string[]] | null;
required?: boolean;
}Component function
In the next step, add the component function to the file. We can destructure props directly and spread the remaining props. Since the component wraps React Native's own TextInput, we import it under the alias RNTextInput.
import type { FieldElementProps } from '@formisch/react-native';
import { TextInput as RNTextInput, Text, View } from 'react-native';
interface TextInputProps extends FieldElementProps {
/* ... */
}
export function TextInput({
label,
input,
errors,
type,
required,
...props
}: TextInputProps) {
// Component implementation
}JSX code
After that, you can add the JSX code to the return statement. Since React Native has no input types, we map the type prop to the matching keyboardType and use secureTextEntry for passwords. React Native has no equivalent of the aria-invalid attribute, so the visible error message below the input is what communicates the invalid state. It also has no way to associate a separate label element with an input, so the label text is passed to the input as its accessibilityLabel for screen readers.
import type { FieldElementProps } from '@formisch/react-native';
import {
type KeyboardTypeOptions,
TextInput as RNTextInput,
Text,
View,
} from 'react-native';
interface TextInputProps extends FieldElementProps {
/* ... */
}
const KEYBOARD_TYPES: Record<TextInputProps['type'], KeyboardTypeOptions> = {
text: 'default',
email: 'email-address',
tel: 'phone-pad',
password: 'default',
url: 'url',
};
export function TextInput({
label,
input,
errors,
type,
required,
...props
}: TextInputProps) {
return (
<View>
{label && (
<Text>
{label} {required && <Text>*</Text>}
</Text>
)}
<RNTextInput
{...props}
value={input ?? ''}
keyboardType={KEYBOARD_TYPES[type]}
autoCapitalize={type === 'email' ? 'none' : undefined}
secureTextEntry={type === 'password'}
accessibilityLabel={label}
/>
{errors && <Text>{errors[0]}</Text>}
</View>
);
}Next steps
You can now build on this code and add styling, for example. You can also follow the procedure to create other components such as a Switch or a picker component.
Final code
Below is an overview of the entire code of the TextInput component.
import type { FieldElementProps } from '@formisch/react-native';
import {
type KeyboardTypeOptions,
TextInput as RNTextInput,
Text,
View,
} from 'react-native';
interface TextInputProps extends FieldElementProps {
type: 'text' | 'email' | 'tel' | 'password' | 'url';
label?: string;
placeholder?: string;
input: string | undefined;
errors: [string, ...string[]] | null;
required?: boolean;
}
const KEYBOARD_TYPES: Record<TextInputProps['type'], KeyboardTypeOptions> = {
text: 'default',
email: 'email-address',
tel: 'phone-pad',
password: 'default',
url: 'url',
};
export function TextInput({
label,
input,
errors,
type,
required,
...props
}: TextInputProps) {
return (
<View>
{label && (
<Text>
{label} {required && <Text>*</Text>}
</Text>
)}
<RNTextInput
{...props}
value={input ?? ''}
keyboardType={KEYBOARD_TYPES[type]}
autoCapitalize={type === 'email' ? 'none' : undefined}
secureTextEntry={type === 'password'}
accessibilityLabel={label}
/>
{errors && <Text>{errors[0]}</Text>}
</View>
);
}Non-text components
The props of the field store are designed for text inputs. React Native's own non-text components — such as Switch, sliders or picker components — do not accept an onChangeText handler, so you cannot spread field.props onto them. Use field.onChange to update the value instead:
import { Switch } from 'react-native';
<Field of={form} path={['newsletter']}>
{(field) => <Switch value={field.input} onValueChange={field.onChange} />}
</Field>;The field.onChange method updates the field value and triggers validation. Your own components can still extend FieldElementProps as shown above: forward the ref to the pressable part of the component and call the onFocus and onBlur handlers when the interaction starts and ends, so that focus, isTouched and blur validation keep working. For more details, see the controlled fields guide.
Next steps
Now that you know how to create reusable input components, continue to the handle submission guide to learn how to process form data when the user submits the form.