Go back

handleChange in TypeScript

Today, I learned that listening to the onChange event is different in TypeScript-based React form components. Normally, the approach I'd take would look like this;

/component/custom-form.jsx
import React from 'react'
 
export default function FormComponent() {
  const [name, setName] = React.useState('')
 
  const handleNameChange = (event) => {
    setName(event.target.value)
  }
 
  const doSomething = () => {
    console.log('do something')
  }
 
  return (
    <div>
      <form>
        <label htmlFor="name">
          Name
          <input
            type="text"
            value={name}
            name="name"
            placeholder="enter your name"
            onChange={handleNameChange}
          />
        </label>
        <button type="button" onClick={doSomething}>
          submit
        </button>
      </form>
    </div>
  )
}

And, that's all one would need to keep track of the state in the name input field of this form component. But it is a bit different with TypeScript, because of its "type safety" perk.

the TypeScript approach

In TypeScript, here's what the component would look like. Pretty much the same thing, but the event handler function is different.

/components/custom-form.tsx
const FormComponent: React.FC = () => {
  const [name, setName] = React.useState('')
 
  const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setUrl(event.target.value)
  }
 
  return (
    <>
      <FormMarkup />
    </>
  )
}

Here's how the snippet — especially the handler — above works;

The handler accepts an argument, (event: React.ChangeEvent<HTMLInputElement>) which is its type signature, indicating that it accepts an event of type React.ChangeEvent<HTMLInputElement>. Accepting the HTMLInputElement means that we're expecting the ChangeEvent interface to dispatch an event whenever there's a change in the value of an HTML input element.

TypeScript will enforce that the event argument passed to handleNameChange is of the correct type and that we can access the value property of the input element without any runtime errors.

The event parameter is an instance of the ChangeEvent method from React. It contains the information about the change event that triggered the callback function — the handler function, in this case.

event.target refers to the element that triggered the event, in this case, an <input> element.

event.target.value is the new value of the input element.

setName(event.target.value) from the useState hook updates the state of the name variable to the new value.

Multiple input fields

You might also want to manage multiple input fields and not have to keep doing React.useState() for every input state. Luckily, there's a way we can co-locate multiple state values in a single useState hook.

Here's what a typical useState() value would look like, if we were to accept multiple values.

/components/custom-form.tsx
const [initialFormValues, setInitialValues] = React.useState({
  email: '',
  fullname: '',
  address: '',
  message: '',
})

To make it type-safe, we can even go ahead to create a component interface for the values our form component should expect.

/components/custom-form.tsx
interface FormComponentProps {
  email: string
  address: string
  message: string
  fullname: string
}
 
export const FormComponent = () => {
  const [initialFormValues, setInitialValues] =
    React.useState<FormComponentProps>({
      email: '',
      fullname: '',
      address: '',
      message: '',
    })
 
  return (
    <div>
      <p>Form title</p>
      // rest of the form markup
    </div>
  )
}

This alone doesn't doesn't manage the input state. We still need a way to track the state of individual input fields. To do this, we'd modify the handleNameChange function. But, this time around to be responsible for all the fields.

/components/custom-form.tsx
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { name, value } = event.target
 
  setInitialValues((prevValues) => ({
    ...initialFormValues,
    [name]: value,
  }))
}

On line two, an object destructuring assignment is used to obtain the name and value properties from the synthetic event, which in turn is used in the setInitialValues setStateAction, with the previous input values spread appropriately.

Textarea fields.

If you try to use the onInputChange function as is on a textarea element, TS will scream at you. So, to avoid this, you need to pass its corresponding type like so:

/components/custom-form.tsx
const onInputChange = (
  event: React.ChangeEvent<HTMLInputElement | HTMLTextareaElement>
) => {
  const { name, value } = event.target
 
  setInitialValues((prevValues) => ({
    ...initialFormValues,
    [name]: value,
  }))
}

my thoughts?

I feel like this is just unnecessarily too "much", Lol, if we're, to be honest. But, the fact that we can define event handlers this way, and ensure type safety in our code is something i can refer to as "cool"

In JavaScript, one could accidentally pass the wrong type of event object to an event handler and not catch the error until runtime.

But, with TypeScript, as they say:

you get compile-time checks that help prevent these kinds of errors.

As always, I'll keep documenting my process with TypeScript here.

Update: No. It isn't unnecessarily too much. If you can, please use TS, It'll save you a lot of time