TypeOfNaN

A React Typescript Change Handler to Rule Them All

Nick Scialli
April 20, 2020

New — Check out my free newsletter on how the web works!

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.

🎓 Learn how the web works

Over my 20 year career in tech, I have noticed that an often-neglected part of interview prep is learning to explain technical concepts clearly. In my free newsletter, How the Web Works, I provide simple, bite-sized explanations for various web topics that you can use in your interview prep (or just to gain a better understanding of the web).

Enter your email below to get started. It's free and you can unsubscribe at any time. You won't regret it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli