React Hook Form with React Native

React Hook Form with React Native

When building forms in React Native, managing form state and validation can be quite a challange. React Hook Form (RHF) is a popular library that simplifies these processes, and has been really popular in React applications.

But when it comes to React Native, one might wonder how to combine it together. It's easy uisng standars modern libraries, like Shadcn/ui, where they have merged beautifuly zod, react-hook-form, and Radix, but we can't exacly use it like that with RN, since some things work different on native applications and web (like forms or accessibility). This is what we are going to do today, adapt the Shadcn/ui implementation for our RN application.

Prerequisites

Before we begin, make sure you have the following:

  • Basic knowledge of React and React Native.
  • A React Native project set up (using Expo or React Native CLI).
  • The following packages installed in your project: react-hook-form, zod (for validating) and nativewind (for styling)


Setting Up the Form Components

We will create several components that form the basis of our form. These components will include Form, FormField, FormLabel, FormMessage, and FormDescription. I will implement all of them in the same file, located in @components/ui/form.tsx , but I will explain them here separetely. Below is the implementation for each component, following the Shadcn design principles.

1. FormProvider & FormField

First, let's create a simple wrapper for the form using FormProvider from RHF.

import classNames from 'classnames';
import { createContext, forwardRef, useContext, useId } from 'react';
import {
  Controller,
  ControllerProps,
  FieldPath,
  FieldValues,
  FormProvider,
  useFormContext,
} from 'react-hook-form';
import { TextProps, View, ViewProps, Text } from 'react-native';

const Form = FormProvider;

type FormFieldContextValue<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName;
};

const FormFieldContext = createContext<FormFieldContextValue>({} as FormFieldContextValue);

const FormField = <
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  ...props
}: ControllerProps<TFieldValues, TName>) => {
  return (
    <FormFieldContext.Provider value={{ name: props.name }}>
      <Controller {...props} />
    </FormFieldContext.Provider>
  );
};

const useFormField = () => {
  const fieldContext = useContext(FormFieldContext);
  const itemContext = useContext(FormItemContext);
  const { getFieldState, formState } = useFormContext();

  const fieldState = getFieldState(fieldContext.name, formState);

  if (!fieldContext) {
    throw new Error('useFormField should be used within <FormField>');
  }

  const { id } = itemContext;

  return {
    id,
    name: fieldContext.name,
    formItemId: `${id}-form-item`,
    formDescriptionId: `${id}-form-item-description`,
    formMessageId: `${id}-form-item-message`,
    ...fieldState,
  };
};

type FormItemContextValue = {
  id: string;
};

const FormItemContext = createContext<FormItemContextValue>({} as FormItemContextValue);        

This is just the basic TypesCript code that we will need for our following components. We are exporting the FormProvider to wrap our form components, and giving it the context. So far nothing changed from Shadcn implementation.

2. FormItem

Here we have to start making some changes. The original implementation uses html items, which are not valid for us on RN:

const FormItem = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
  const id = React.useId()
 
  return (
    <FormItemContext.Provider value={{ id }}>
      <div ref={ref} className={cn("space-y-2", className)} {...props} />
    </FormItemContext.Provider>
  )
})
FormItem.displayName = "FormItem"
        

We will have to change the html elements for RN elements

const FormItem = forwardRef<View, ViewProps>(({ className, ...props }, ref) => {
  const id = useId();

  return (
    <FormItemContext.Provider value={{ id }}>
      <View ref={ref} className={classNames('flex-col gap-2', className)} {...props} />
    </FormItemContext.Provider>
  );
});
FormItem.displayName = 'FormItem';        


3. FormLabel

Shadcn implementation:

const FormLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
  const { error, formItemId } = useFormField()
 
  return (
    <Label
      ref={ref}
      className={cn(error && "text-destructive", className)}
      htmlFor={formItemId}
      {...props}
    />
  )
})
FormLabel.displayName = "FormLabel"        

Now we addapt it to RN

const FormLabel = forwardRef<Text, TextProps>(({ className, ...props }, ref) => {
  const { error, formItemId } = useFormField();

  return (
    <Text
      ref={ref}
      accessibilityLabel={formItemId}
      className={classNames(error && 'text-xl font-semibold', className)}
      {...props}
    />
  );
});

FormLabel.displayName = 'FormLabel';        

4. FormControl

Shadcn implementation

const FormControl = React.forwardRef<
  React.ElementRef<typeof Slot>,
  React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
 
  return (
    <Slot
      ref={ref}
      id={formItemId}
      aria-describedby={
        !error
          ? `${formDescriptionId}`
          : `${formDescriptionId} ${formMessageId}`
      }
      aria-invalid={!!error}
      {...props}
    />
  )
})
FormControl.displayName = "FormControl"        

Adaptation:

const FormControl = forwardRef<View, ViewProps>(({ ...props }, ref) => {
  const { formItemId } = useFormField();

  return <View ref={ref} id={formItemId} {...props} />;
});

FormControl.displayName = 'FormControl';        

I'm not too concered about the accessibility here, I'd rather docus accessibility on the inputs level, but if you are you can read more on the docs

5. FormMessage

The FormMessage component displays validation messages.

const FormMessage = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
  const { error, formMessageId } = useFormField()
  const body = error ? String(error?.message) : children
 
  if (!body) {
    return null
  }
 
  return (
    <p
      ref={ref}
      id={formMessageId}
      className={cn("text-sm font-medium text-destructive", className)}
      {...props}
    >
      {body}
    </p>
  )
})
FormMessage.displayName = "FormMessage"
        

Adaptation:

const FormMessage = forwardRef<Text, TextProps>(({ children, className, ...props }, ref) => {
  const { error } = useFormField(); // Obtain the error from your form context
  const body = error ? String(error.message) : children;

  if (!body) {
    return null;
  }

  return (
    <Text ref={ref} className={classNames('text-red-500', className)} {...props}>
      {body}
    </Text>
  );
});
        

Some classNames do not work on RN, like text-muted-foreground or text-destructive as examples and we need some workarounds.

5. FormDescription

This component provides additional context for the fields.

const FormDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
  const { formDescriptionId } = useFormField()
 
  return (
    <p
      ref={ref}
      id={formDescriptionId}
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
})
FormDescription.displayName = "FormDescription"        

Adaptation:

const FormDescription = forwardRef<Text, TextProps>(({ className, ...props }, ref) => {
  const { formDescriptionId } = useFormField();

  return (
    <Text
      ref={ref}
      accessibilityLabel={formDescriptionId}
      className={classNames('text-md text-neutral-600', className)}
      {...props}
    />
  );
});

FormDescription.displayName = 'FormDescription';        

Using the Form Component

Now that we have the individual components set up, we can create the main form component as it is on the shadcn form

The main difference is that you will have to use the RN components for the inputs, as an example:

import { TextInput } from 'react-native
<Form {...form}>
   <FormField
                control={form.control}
                name="username"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Username</FormLabel>
                    <FormControl>
                      <TextInput
                        className="rounded-md border border-gray-400 p-2"
                        placeholder="JohnDoe-74"
                        {...field}
                      />
                    </FormControl>
                    <FormDescription>
                      This is your display name
                    </FormDescription>
                    <FormMessage />
                  </FormItem>
                )}
              />
        ...
    <Button title="Submit" onPress={form.handleSubmit(onSubmit)} />
</Form>        


Conclusion

By following the Shadcn design principles and leveraging the power of React Hook Form, you can create a well-structured and easily manageable form in your React Native applications. The components outlined in this article will help you implement forms with robust validation and a clean user interface.

Additional Resources

Feel free to explore further and customize the components to meet your specific needs. Happy coding!

To view or add a comment, sign in

More articles by Gerard Siles

  • Type safe environment variables in Node

    Environment variables are crucial for configuring applications, but managing them safely and efficiently can be…

  • Understanding Test-Driven Development (TDD) and Its Practical Applications

    Test-Driven Development (TDD) is a software development approach that emphasizes writing tests before writing the…

  • MUI & Motion-Framer

    Have you ever wondered how to combine the power of the easy animations that Framer Motion offers with Material UI…

    2 Comments
  • Islandia: Guia de viaje

    si estais planeando vuestro viaje a Islandia, aqui teneis todas las claves para ayudaros a decidir que ver y mis…

Explore content categories