Loading...

Skip to main content

useForm hook

Setting up the reducer

Our useForm hook only takes in one parameter: the initialState of the form. For this hook, I’m going to use a reducer to keep track of and update the form data. Let’s create an "UPDATE" action type that takes in a field name and value, and updates our state accordingly.

useForm.js

import { useReducer } from "react"; export function useForm(initialState) { function reducer(state, action) { switch (action.type) { case "UPDATE": return { ...state, [action.field]: action.value, }; default: return state; } } const [formData, dispatch] = useReducer(reducer, initialState); }

Simplifying the code

From there, we could export formData and dispatch from our useForm hook, and use them in our Form component like so:

Form.jsx

<input name="fullName" value={formData.fullName} onChange={(e) => dispatch({ type: "UPDATE", name: "fullName", value: e.target.value, }) } />

But if we have a lot of fields, this might end up becoming a bit repetitive. So let’s go back to our useHook file and add a new updateFormField function that we can use instead of dispatch.

useForm.js

const updateFormField = (e) => { dispatch({ type: "UPDATE", inputType: e.target.type, value: e.target.value, field: e.target.name, }); }; return { formData, updateFormField };

Instead of just receiving the field’s name and value, this function takes in the onChange event as a parameter. This allows us to get the info we need from the event, and simplify our input to this:

Form.js

<input name="fullName" value={formData.fullName} onChange={(e) => updateFormField(e)} />

Handling number inputs

There’s one more improvement we can make to our code: in our updateFormField function, you’ll notice that we dispatch an inputType variable that gets its value from the input’s type attribute.

This allows us to parse the value of the input’s change event to a number for number inputs:

Form.js

function reducer(state, action) { switch (action.type) { case "UPDATE": return { ...state, [action.field]: action.inputType === "number" ? parseInt(action.value) : action.value, }; default: return state; } }

Of course, this might not cover 100% of use cases; for example, you might need a fractional number rather than an integer. But this hook is a good starting point.

The hook

This is what our base hook will look like.

type Action = { type: "UPDATE"; field: string; inputType?: string; value: string; }; export function useForm<FormData>(initialState: FormData): { formData: FormData; updateFormField: ( e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> ) => void; } { function reducer(state: FormData, action: Action) { switch (action.type) { case "UPDATE": return { ...state, [action.field]: action.inputType === "number" ? parseInt(action.value) : action.value, }; default: return state; } } const [formData, dispatch] = useReducer(reducer, initialState); const updateFormField = ( e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement> ) => { dispatch({ type: "UPDATE", inputType: e.target.type, value: e.target.value, field: e.target.name, }); }; return { formData, updateFormField }; }
import { useReducer } from "react"; export function useForm(initialState) { function reducer(state, action) { switch (action.type) { case "UPDATE": return { ...state, [action.field]: action.inputType === "number" ? parseInt(action.value) : action.value, }; default: return state; } } const [formData, dispatch] = useReducer(reducer, initialState); const updateFormField = (e) => { dispatch({ type: "UPDATE", inputType: e.target.type, value: e.target.value, field: e.target.name, }); }; return { formData, updateFormField }; }

Usage

To make use of the useForm hook in a form component, we just need to import it and set its initial state:

import { useForm } from "hooks/useForm"; type FormDataType = { fullName: string; birthYear: number; }; export default function Form() { const { formData, updateFormField } = useForm<FormDataType>({ fullName: "John Doe", birthYear: 1990, }); return ( <form> <label htmlFor="fullName">Full name</label> <input type="text" name="fullName" value={formData.fullName} onChange={(e) => updateFormField(e)} required /> <label htmlFor="birthYear">Birth year</label> <input type="number" name="birthYear" value={formData.birthYear} onChange={(e) => updateFormField(e)} required /> <input type="submit" value="Submit" /> </form> ); }
import { useForm } from "hooks/useForm"; export default function Form() { const { formData, updateFormField } = useForm({ fullName: "John Doe", birthYear: 1990, }); return ( <form role="search" action="/search"> <label htmlFor="fullName">Full name</label> <input type="text" name="fullName" value={formData.fullName} onChange={(e) => updateFormField(e)} required /> <label htmlFor="birthYear">Birth year</label> <input type="number" name="birthYear" value={formData.birthYear} onChange={(e) => updateFormField(e)} required /> <input type="submit" value="Submit" /> </form> ); }

Demo

import "./styles.css";

import { useForm } from "./hooks/useForm";

type FormDataType = {
  fullName: string;
  birthYear: number;
};

const initialData: FormDataType = {
  fullName: "John Doe",
  birthYear: 1990
};

export default function Form() {
  const { formData, updateFormField } = useForm<FormDataType>(initialData);

  return (
    <form>
      <label htmlFor="fullName">Full name</label>
      <input
        type="text"
        name="fullName"
        autoComplete="name"
        value={formData.fullName}
        onChange={(e) => updateFormField(e)}
        required
      />
      <label htmlFor="birthYear">Birth year</label>
      <input
        type="number"
        name="birthYear"
        autoComplete="bday-year"
        value={formData.birthYear}
        onChange={(e) => updateFormField(e)}
        required
      />
      <input type="submit" value="Submit" />
      <output name="result" htmlFor="fullName birthYear">
        {JSON.stringify(formData)}
      </output>
    </form>
  );
}