DEV Community

loading...

Building reusable form components in React

rushi444 profile image Rushi Arumalla ・2 min read

I've read and watched many demos that show you how to create form components with a form and component library, but wrapping inputs in form controls can get quite repetitive especially if you are working with a large application. I'll be using CRA(create-react-app), Chakra UI's form components and react-hook-form for this demo (Feel free to use any libraries you like ex. Formik, Material UI, etc.).

To follow along run this command inside of your react app:

yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion react-hook-form
Enter fullscreen mode Exit fullscreen mode

Here's a traditional pattern you will see for a form input:

import { useForm } from "react-hook-form";
import {
  FormControl,
  FormLabel,
  FormErrorMessage,
  Input,
} from "@chakra-ui/react";

function App() {
  const { register, errors, handleSubmit } = useForm({
    defaultValues: {
      name: "",
    },
  });

  const onSubmit = (values) => {
    console.log("Form Submitted", { values });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <FormControl>
          <FormLabel>Name: </FormLabel>
          <Input
            id="name"
            name="name"
            ref={register({ required: "Please enter your name" })}
          />
          <FormErrorMessage>{errors.name}</FormErrorMessage>
        </FormControl>
      </div>
      <button type='submit'>Submit</button>
    </form>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

You might be thinking that it doesn't look too bad. But what if you had > 10 fields on this screen, or had to do this over and over across your application. We can clean this up by creating an input that you could reuse all over your application. Let's create a file called InputField.js and add the below code.

import { useController } from "react-hook-form";
import {
  FormControl,
  FormErrorMessage,
  FormLabel,
  Input,
} from "@chakra-ui/react";

const useMetaError = ({ invalid, isTouched }) => {
  const errorMessage = (isTouched && invalid?.message) || "";
  return {
    errorMessage,
    hasError: !!errorMessage,
  };
};

export const InputField = (props) => {
  const { type = "text", label } = props;
  const { field, meta } = useController(props);
  const { errorMessage, hasError } = useMetaError(meta);
  return (
    <div>
      <FormControl isInvalid={hasError}>
        <FormLabel>{label}</FormLabel>
        <Input {...field} type={type} />
        <FormErrorMessage>{errorMessage}</FormErrorMessage>
      </FormControl>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

So let's start with useController, a hook that lets us create a controlled input, giving us access to the form we created in App.js. Using the meta prop we can create our own function to handle the error useMetaError, which will return an error if the input has been touched and has an error. The rest is just passing in props from our form, so let's take a look at how this looks in App.js:

import { useForm } from "react-hook-form";
import { InputField } from "./inputField";

function App() {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      name: "",
    },
  });

  const onSubmit = (values) => {
    console.log("Form Submitted", { values });
  };
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <InputField
        name="name"
        label="Name: "
        control={control}
        rules={{ required: "Please enter your name" }}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now all we have to do is pass in the control that we get from useForm instead and our InputField.js will handle the rest.

And boom 🚀 , we can now use our InputField component inside of any form handled by react-hook-form in our application!

Discussion (0)

pic
Editor guide