Forms
Forms with TanStack Form
Section titled “Forms with TanStack Form”This template uses TanStack Form for form state management, providing a powerful and flexible solution with excellent TypeScript support.
Why TanStack Form?
Section titled “Why TanStack Form?”- Type-safe: Full TypeScript support with strong typing
- Flexible validation: Works seamlessly with Zod schemas
- Performance: Efficient re-rendering with granular subscriptions
- Framework agnostic: Works with React Native
- Simple API: Intuitive field and form management
Basic Form Example
Section titled “Basic Form Example”Here’s a simple login form using TanStack Form:
import { useForm } from '@tanstack/react-form';import * as z from 'zod';import { Button, Input, View } from '@/components/ui';import { getFieldError } from '@/components/ui/form-utils';
const schema = z.object({ email: z .string() .min(1, 'Email is required') .email('Invalid email format'), password: z .string() .min(1, 'Password is required') .min(6, 'Password must be at least 6 characters'),});
export function LoginForm({ onSubmit }) { const form = useForm({ defaultValues: { email: '', password: '', }, validators: { onChange: schema as any, }, onSubmit: async ({ value }) => { onSubmit(value); }, });
return ( <View className="p-4"> <form.Field name="email" children={(field) => ( <Input label="Email" value={field.state.value} onBlur={field.handleBlur} onChangeText={field.handleChange} error={getFieldError(field)} /> )} />
<form.Field name="password" children={(field) => ( <Input label="Password" secureTextEntry value={field.state.value} onBlur={field.handleBlur} onChangeText={field.handleChange} error={getFieldError(field)} /> )} />
<form.Subscribe selector={(state) => [state.isSubmitting]} children={([isSubmitting]) => ( <Button label="Login" loading={isSubmitting} onPress={form.handleSubmit} /> )} /> </View> );}Form Validation with Zod
Section titled “Form Validation with Zod”TanStack Form integrates seamlessly with Zod for schema validation. Define your schema outside the component to prevent recreation on each render:
const schema = z.object({ email: z .string() .min(1, 'Email is required') .email('Invalid email format'), password: z .string() .min(6, 'Must be at least 6 characters'),});
const form = useForm({ validators: { onChange: schema as any, // Real-time validation as user types }, // ... other options});The onChange validator runs your Zod schema on every field change, providing immediate feedback to users.
Field Error Handling
Section titled “Field Error Handling”The template includes a getFieldError utility function that extracts error messages from form fields:
import { getFieldError } from '@/components/ui/form-utils';
<form.Field name="email" children={(field) => ( <Input error={getFieldError(field)} // ... other props /> )}/>This utility:
- Only shows errors after a field is touched
- Handles both string errors and Zod error objects
- Extracts the first error message for display
Form State Management
Section titled “Form State Management”Access form state using the form.Subscribe component with selective subscriptions to optimize re-renders:
<form.Subscribe selector={(state) => [state.isValid, state.isSubmitting]} children={([isValid, isSubmitting]) => ( <Button disabled={!isValid} loading={isSubmitting} onPress={form.handleSubmit} /> )}/>Available state properties:
isValid- Whether the form passes validationisSubmitting- Whether form is currently submittingcanSubmit- Whether form can be submitted (touched + valid)isDirty- Whether form values have changed from defaults
Field API
Section titled “Field API”Each field render prop provides access to field state and handlers:
<form.Field name="email" children={(field) => { // Available properties: field.state.value // Current field value field.handleChange // Update value handler field.handleBlur // Blur event handler field.state.meta.isTouched // Whether field has been interacted with field.state.meta.errors // Array of validation errors
return <Input {...props} /> }}/>Working with Select Components
Section titled “Working with Select Components”The same pattern applies to Select components:
<form.Field name="category" children={(field) => ( <Select label="Category" value={field.state.value} onSelect={(value) => field.handleChange(value)} options={categories} error={getFieldError(field)} /> )}/>Note: Use onSelect instead of onChangeText for Select components.
Real-World Example: Form with API Integration
Section titled “Real-World Example: Form with API Integration”See src/features/feed/add-post-screen.tsx for a complete example that demonstrates:
- Form submission with TanStack Query mutation
- Loading states from API calls
- Success and error notifications with react-native-flash-message
- Multiline text input usage
export default function AddPost() { const { mutate: addPost, isPending } = useAddPost();
const form = useForm({ defaultValues: { title: '', body: '', }, validators: { onChange: schema as any, }, onSubmit: ({ value }) => { addPost( { ...value, userId: 1 }, { onSuccess: () => { showMessage({ message: 'Post added successfully', type: 'success', }); }, onError: () => { showErrorMessage('Error adding post'); }, }, ); }, });
return ( <View className="flex-1 p-4"> <form.Field name="title" children={(field) => ( <Input label="Title" value={field.state.value} onBlur={field.handleBlur} onChangeText={field.handleChange} error={getFieldError(field)} /> )} />
<form.Field name="body" children={(field) => ( <Input label="Content" multiline value={field.state.value} onBlur={field.handleBlur} onChangeText={field.handleChange} error={getFieldError(field)} /> )} />
<form.Subscribe selector={(state) => [state.isSubmitting]} children={([isSubmitting]) => ( <Button label="Add Post" loading={isPending || isSubmitting} onPress={form.handleSubmit} /> )} /> </View> );}Best Practices
Section titled “Best Practices”- Define Zod schema outside component - Prevents recreation on each render and improves performance
- Use getFieldError utility - Consistent error extraction across all forms
- Subscribe to specific state - Use selector to optimize re-renders by only subscribing to needed state
- Validate on change - Provide immediate feedback to users with
onChangevalidator - Show errors after touch - Better UX than showing errors immediately on render
- Combine loading states - When using with TanStack Query, combine
isSubmittingandisPendingfor accurate loading state
Common Patterns
Section titled “Common Patterns”Optional Fields
Section titled “Optional Fields”const schema = z.object({ name: z.string().optional(), email: z.string().email(),});Conditional Validation
Section titled “Conditional Validation”const schema = z.object({ acceptTerms: z.boolean(), email: z.string().email(),}).refine( (data) => data.acceptTerms === true, { message: 'You must accept the terms', path: ['acceptTerms'] });Async Validation
Section titled “Async Validation”const form = useForm({ validators: { onChange: schema, onSubmitAsync: async ({ value }) => { // Async validation logic const isUnique = await checkEmailUnique(value.email); if (!isUnique) { return { fields: { email: 'Email already exists', }, }; } }, },});Handling Keyboard
Section titled “Handling Keyboard”The template comes with react-native-keyboard-controller pre-installed and configured to handle the keyboard. You only need to check the documentation and use the appropriate approach for your use case. (Note that we already added the KeyboardProvider to the layout in the root file)
Make sure to check the following video for more details on how to handle keyboard in React Native: