Git Product home page Git Product logo

react-ts-forms-abstraction's Introduction

BONUS: React Forms Abstraction

Learning Goals

  • Make our form logic more reusable by creating a dynamic onChange event handler

Code Along

If you want to code along there is starter code in the src folder. Make sure to run npm install && npm start to see the code in the browser.

Form State

Let's talk about the onChange event we had set up in the initial version of our Form component. If we look at the original code:

import { useState } from "react";

function Form() {
  const [firstName, setFirstName] = useState("Hasung");
  const [lastName, setLastName] = useState("Kim");

  function handleFirstNameChange(event: React.ChangeEvent<HTMLInputElement>) {
    setFirstName(event.target.value);
  }

  function handleLastNameChange(event: React.ChangeEvent<HTMLInputElement>) {
    setLastName(event.target.value);
  }

  return (
    <form>
      <input type="text" onChange={handleFirstNameChange} value={firstName} />
      <input type="text" onChange={handleLastNameChange} value={lastName} />
      <button type="submit">Submit</button>
    </form>
  );
}

export default Form;

We can imagine that adding more input fields to this form is going to get repetitive pretty fast. For every new input field, we'd need to add:

  • a new state variable by calling useState() to hold the value of that input
  • a new handleChange function to update that piece of state

As a first refactor, let's use useState just once, and make an object representing all of our input fields. We will also need to update our onChange handlers and the variable names in our JSX accordingly:

function Form() {
  const [formData, setFormData] = useState({
    firstName: "Hasung",
    lastName: "Kim",
  });

  function handleFirstNameChange(event: React.ChangeEvent<HTMLInputElement>) {
    setFormData({
      ...formData,
      firstName: event.target.value,
    });
  }

  function handleLastNameChange(event: React.ChangeEvent<HTMLInputElement>) {
    setFormData({
      ...formData,
      lastName: event.target.value,
    });
  }

  return (
    <form>
      <input
        type="text"
        onChange={handleFirstNameChange}
        value={formData.firstName}
      />
      <input
        type="text"
        onChange={handleLastNameChange}
        value={formData.lastName}
      />
    </form>
  );
}

Since our initial state is an object, in order to update state in our onChange handlers, we have to copy all the key/value pairs from the current version of that object into our new state โ€” that's what the spread operator here is doing:

setFormData({
  // formData is an object, so we need to copy all the key/value pairs
  ...formData,
  // we want to overwrite the lastName key with a new value
  lastName: event.target.value,
});

Now, we just have one object in state to update whenever an input field changes.

Our change handlers are still a bit verbose, however. Since each one is changing a different value in our state, we've got them separated here. You can imagine that once we've got a more complicated form, this approach may result in a very cluttered component.

Instead of writing separate functions for each input field, we could actually condense this down into a single, more reusable, function. Since event is being passed in as the argument, we have access to the event.target attributes that may be present.

If we give our inputs name attributes, we can access them as event.target.name:

<input
  type="text"
  name="firstName"
  value={formData.firstName}
  onChange={handleFirstNameChange}
/>
<input
  type="text"
  name="lastName"
  value={formData.lastName}
  onChange={handleLastNameChange}
/>

As long as the name attributes of our <input> fields match the keys in our state, we can write a generic handleChange function like so:

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  // name is the KEY in the formData object we're trying to update
  const name = event.target.name;
  // value is the same as before, it is the input value given by the user
  const value = event.target.value;

  setFormData({
    ...formData,
    [name]: value,
  });
}

Then, if we connect this new function to both of our inputs, they will both correctly update state. Why? Because for the first input, event.target.name is set to firstName, while in the second input, it is set to lastName. Each input's name attribute will change which part of state is actually updated!

Now, if we want to add a new input field to the form, we just need to add two things:

  • a new key/value pair in our formData state, and
  • a new <input> field where the name attribute matches our new key

We can take it one step further, and also handle checkbox inputs in our handleChange input. Since checkboxes have a checked attribute instead of the value attribute, we'd need to check what type our input is in order to get the correct value in state.

Here's what the final version of our Form component looks like:

import { useState } from "react";

function Form() {
  const [formData, setFormData] = useState({
    firstName: "Hasung",
    lastName: "Kim",
    admin: false,
  });

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    const name = event.target.name;
    let value: string | boolean = event.target.value;

    // use `checked` property of checkboxes instead of `value`
    if (event.target.type === "checkbox") {
      value = event.target.checked;
    }

    setFormData({
      ...formData,
      [name]: value,
    });
  }

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    console.log(formData);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="firstName"
        onChange={handleChange}
        value={formData.firstName}
      />
      <input
        type="text"
        name="lastName"
        onChange={handleChange}
        value={formData.lastName}
      />
      <input
        type="checkbox"
        name="admin"
        onChange={handleChange}
        checked={formData.admin}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

export default Form;

Depending on what input elements you're working with, you might also have to add some additional logic to handle things like number fields (using parseInt or parseFloat) and other data types to ensure your form state is always in sync with your components.

Additionally, notice how we had to specifically type our value variable with a union type of string | boolean. This is because we initialize the value with event.target.value by default, so TypeScript will correctly infer that it is of type string. This becomes a problem when we want to re-set value to equal event.target.checked if our input turns out to be a checkbox, which holds a boolean value instead of a string. The explicit union type solves this problem.

Conclusion

Working with controlled forms in React involves writing a lot of boilerplate code. We can abstract away some of that boilerplate by making our change handling logic more abstract.

Note: Working with complex forms can get quite challenging! If you're using a lot of forms in your application, it's worth checking out some nice React libraries like react hook form to handle some of this abstraction. You can also use them to add custom client-side validation to your forms.

Resources

react-ts-forms-abstraction's People

Contributors

jlboba avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.