TypeOfNaN

A React Typescript Change Handler to Rule Them All

Nick Scialli
April 20, 2020

In React with Typescript, you may be tempted to roll individual change handlers for each field in a component. Today I’ll show you how to avoid this redundant work and write just one change handler!

A Simple Use Case

Here’s a simple use case: we have a form in React with two text inputs and a checkbox input. These inputs will populate a User object, which will have the following types:

type User = {
  name: string;
  age: number;
  admin: boolean;
};

Let’s see how this might look in the context of a React component. We’ll use the useState hook to maintain internal state for the user object.

import React, { useState } from 'react';

type User = {
  name: string;
  age: number | null;
  admin: boolean;
};

const defaultUser: User = {
  name: '',
  age: null,
  admin: false,
};

function App() {
  const [user, setUser] = useState(defaultUser);

  return (
    <div>
      <input value={user.name} />
      <input value={user.age || ''} />
      <input type="checkbox" checked={user.admin} />
    </div>
  );
}

Handling Changes

But how to handle changes for each property? Well, we could create a different change handler for each input, but that would be redundant. Let’s create a single onUserChange that set’s the correct prop for each input.

import React, { useState } from 'react';

type User = {
  name: string;
  age: number | null;
  admin: boolean;
};

const defaultUser: User = {
  name: '',
  age: null,
  admin: false,
};

function App() {
  const [user, setUser] = useState(defaultUser);

  const onUserChange = <P extends keyof User>(prop: P, value: User[P]) => {
    setUser({ ...user, [prop]: value });
  };

  return (
    <div>
      <input
        value={user.name}
        onChange={(e) => {
          onUserChange('name', e.target.value);
        }}
      />
      <input
        value={user.age || ''}
        onChange={(e) => {
          onUserChange('age', parseInt(e.target.value));
        }}
      />
      <input
        type="checkbox"
        checked={user.admin}
        onChange={() => {
          onUserChange('admin', !user.admin);
        }}
      />
    </div>
  );
}

The secret sauce here is the generic we use in the onUserChange handler. By saying that prop is of type P where P extends keyof User, we can typecheck the second argument of onUserChange based on the first argument. Then, when we setUser, the typescript compiler is happy with our typings.

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at the Microsoft.

© 2022 Nick Scialli