Form

A Form component with React Hook Form and Zod.

Docs

Forms are tricky. They are one of the most common things you'll build in a mobile or web application, but also one of the most complex.

Well-designed mobile forms are:

  • Well-structured and semantically correct.
  • Well-styled and consistent with the rest of the application.
  • Accessible with ARIA attributes and proper labels.
  • Easy to use and understand.
  • Easy to maintain and test.
  • Easy to style and theme.

In this guide, we will take a look at building forms with react-hook-form and zod. We're going to use a <FormField> component to compose accessible forms for your mobile application.

Features

The <Form /> component is a wrapper around the react-hook-form library. It provides a few things:

  • Composable components for building forms.
  • A <FormField /> component for building controlled form fields.
  • Form validation using zod.
  • Handles accessibility and error messages.
  • Uses React.useId() for generating unique IDs.
  • Applies the correct aria attributes to form fields based on states.
  • Bring your own schema library. We use zod but you can use anything you want.
  • You have full control over the markup and styling.

Anatomy


<Form>
  <FormField
    control={...}
    name="..."
    render={() => (
      <FormItem>
        <FormLabel />
        <FormControl>
          { /* Your form field */}
        </FormControl>
        <FormDescription />
        <FormMessage />
      </FormItem>
    )}
  />
</Form>

Example


const form = useForm()

<FormField
control={form.control}
name="username"
render={({ field }) => (
  <FormItem>
    <FormLabel>Username</FormLabel>
    <FormControl>
      <Input placeholder="fmenezes" {...field} />
    </FormControl>
    <FormDescription>This is your public display name.</FormDescription>
    <FormMessage />
  </FormItem>
)}
/>

Installation

Terminal
npx flexnative-cli add "https://registry.flexnative.com/c/form.json"

Usage

Create a form schema

Define the shape of your form using a Zod schema. You can read more about using Zod in the Zod documentation.

import { z } from "zod" 

const formSchema = z.object({
username: z.string().min(2).max(50),
})

Define a form

Use the useForM hook from react-hook-form to create a form.

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

const formSchema = z.object({
username: z.string().min(2, {
  message: "Username must be at least 2 characters.",
}),
})

export function ProfileForm() {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({ 
  resolver: zodResolver(formSchema), 
  defaultValues: { 
    username: "", 
  }, 
}) 

// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) { 
  // Do something with the form values.
  // ✅ This will be type-safe and validated.
  console.log(values) 
  } 
}

Since FormField is using a controlled component, you need to provide a default value for the field. See the React Hook Form docs to learn more about controlled components.

Build your form

Use the FormField component to create a form field.

All code
import { StyleSheet } from "react-native"; 

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

import { z } from "zod";
import {   
Form,  
FormControl, 
FormField, 
FormItem, 
FormLabel, 
FormMessage, 
} from "@/components/ui/form"; 
import { TextInput } from "@/components/ui/text-input"; 
import { View } from "@/components/ui/view"; 
import { Text } from "@/components/ui/text"; 
import { Button } from "@/components/ui/button"; 
import { ScrollView } from "@/components/ui/scroll-view"; 

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
});

export default function ProfileForm() {
  const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  values: {
    username: "",
  },
});

function onSubmit(values: z.infer<typeof formSchema>) {
  // Do something with the form values.
  // ✅ This will be type-safe and validated.
  console.log(values);
}

return ( 
  <ScrollView
    contentContainerStyle={styles.scrollContainer}
  >
    <View style={styles.infoContainer}>
      <Text style={styles.mainTitle}>Example Form</Text>
      <Text style={styles.description}>
        Enter your username and see the magic
      </Text>
    </View>
    <Form {...form}>
      <View style={styles.formContainer}>
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => ( 
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <TextInput
                  placeholder="Enter your username"
                  value={field.value}
                  onChangeText={field.onChange}
                />
              </FormControl>
              <FormMessage />
            </FormItem> 
          )}
        />
      </View>
      <Button
        label="Submit"
        onPress={form.handleSubmit(onSubmit)}
      />
    </Form>
  </ScrollView> 
); 
} 

const styles = StyleSheet.create({ 
scrollContainer: { 
  flexGrow: 1, 
  justifyContent: "center", 
  padding: 16, 
  gap: 16, 
}, 
infoContainer: { 
  gap: 10, 
}, 
mainTitle: { 
  fontSize: 28, 
  fontWeight: "bold", 
}, 
description: { 
  fontSize: 16, 
}, 
formContainer: { 
  gap: 10, 
}, 
}); 

Done

That's it. You now have a fully accessible form that is type-safe with client-side validation.

Examples

See the following links for more examples on how to use the <Form /> component with other components:

API

Form

The main wrapper component that provides form context.


PropTypeDescriptionDefault
childrenReactNodeContent of the form.-
...propsFormProviderPropsAll props from React Hook Form's FormProvider.-

FormField

A controlled form field component.

PropTypeDescriptionDefault
controlControlForm control from React Hook Form.-
namestringField name.-
render(field: FieldProps) => JSXRender function for the field.-

FormItem

Container for form field components.

PropTypeDescriptionDefault
styleStyleProp<ViewStyle>Additional styles for the container.-
...propsViewPropsAll props from React Native's View component.-

FormLabel

Label component for form fields.

PropTypeDescriptionDefault
styleStyleProp<TextStyle>Additional styles for the label.-
...propsTextPropsAll props from React Native's Text component.-

FormControl

Wrapper for the form input component.

PropTypeDescriptionDefault
styleStyleProp<ViewStyle>Additional styles for the control wrapper.-
...propsViewPropsAll props from React Native's View component.-

FormDescription

Optional description text below the form field.

PropTypeDescriptionDefault
styleStyleProp<TextStyle>Additional styles for the description.-
...propsTextPropsAll props from React Native's Text component.-

FormMessage

Component to display validation error messages.

PropTypeDescriptionDefault
styleStyleProp<TextStyle>Additional styles for the error message.-
...propsTextPropsAll props from React Native's Text component.-