DEV Community

Cover image for Abstracting Input Change Logic
sheenasany
sheenasany

Posted on

Abstracting Input Change Logic

Cut down on Boilerplate Forms!

As a budding programmer, we learn the easier routes of how to get things to work on React before stepping it up a notch and moving on to cleaner, drier, more concise code. Well, as a budding programmer you may be familiar with how to create a form using React by now, right? And I’m sure you’re well acquainted with how to set the state of each input value or how to create a function to capture all those input values in order to change the state, yes? And I’m sure you’re following best coding practices by keeping state and the function to handle that state in the same component. Or that you’re also setting the function to handle the onChange and Submit event outside of the JSX, correct? Or even that you're using controlled inputs on your form, right? Yes, I’m sure we’re all up to speed on that and that your code kinda looks like this then:

Starter Code

import React, { useState } from "react";

function Form() {

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [avatar, setAvatar] = useState("");
  const [mailingList, setMailingList] = useState(false);

  const handleUsername = (e) => {
    setUsername(e.target.value)
  } 

  const handlePassword = (e) => {
    setPassword(e.target.value)
  }

  const handleAvatar = (e) => {
    setAvatar(e.target.value)
  } 

  const handleMailingList = (e) => {
    setMailingList(e.target.checked)
  } 

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = {
     username,
     password,
     avatar,
     accountType,
     newsletter
  }
  console.log(formData);
  }



  return (
    <form onSubmit={handleSubmit}>
      <h1>Create an Account</h1>
      <label htmlFor="username">Username</label>
      <input
        type="text"
        id="username"
        value={username}
        onChange={handleUsername}
      />

      <label htmlFor="password">Password</label>
      <input
        type="password"
        id="password"
        value={password}
        onChange={handlePassword}
      />

      <label htmlFor="avatar">Avatar Image</label>
      <input
        type="text"
        id="avatar"
        value={avatar}
        onChange={handleAvatar}
      />
      <img src="https://i.pinimg.com/originals/0a/dd/87/0add874e1ea0676c4365b2dd7ddd32e3.jpg"
        alt="Avatar preview"
      />

      <label>
        Join our Mailing List!
        <input
          type="checkbox"
          id="mailingList"
          checked={mailingList}
          onChange={handleMailingList}
        />

      </label>
      <input type="submit" value="Sign Up" />
    </form>

  )
}

export default Form
Enter fullscreen mode Exit fullscreen mode

Whew! What a beast... This is typical boiler plate, cut, copy, alt + click all those lines, and paste form essentially. But as a beginner, this is great to know! Get it down to the point where it's muscle memory. Then you're ready to move on to more dynamic forms that can adapt if you want to edit the form to capture more user data. Let's go into detail with this.

Set The State

First start with the all those little slices of state for each input field. We're going to take all those states and mold them into one by initializing the state into an object with keys that each have an initial slice of state to them. Let's call that new slice of state formData as it will be capturing all of our forms data. Like so:


Difference between pre-boiler plate and condensing code into one slice of state


As you can see, we're taking all of those slices of state from the orange box, putting them under one slice of state in the yellow box, and initializing it as an object with key values set to the empty strings and false state for the mailing list as we had before when they were all sitting pretty in their own states. It's already starting to look a little less cluttered, huh? Let's move on.

Next thing we need to do is look to our JSX where the values are set within each input. As you can see we previously set the value to the state we initially had, but now that we've condensed our state to formData, let us now plug in that state and use dot notation to set our state the the key value of username. We'll need to plug this in for all of our values.


Highlighting the change of code in values


I'd like to point out here that the value for a checkbox is checked instead of value so keep that in mind as we move forward.

Deconstruct the Handler Functions

Best practice dictates that we keep state and their function handlers wrapped in the same component so we are following that here. Next, let's hone our attention to the very same handler functions that we had for each of those slices of state set to capture the input values when the onChange event gets fired off every time a user submits a new value within that particular field. As you can see, they take up quite a bit of space as we're setting that state for EACH input field. Imagine if we had a much larger form taking in A LOT more information from the user. Our code would be a bit more exhaustive and repetitive. Let's fix that.

First thing we're going to do is create a new function that will handle all the onChange events for each input field. We'll call it handleChange. This next part gets a little tricky because our setter function, setFormData, expects an object so we need to call it with an object. Therefore, we need to use curly braces when we invoke the setter function. What we want to do is have that object contain all of the data from the formData variable so we'll be using the spread operator to copy all the original data from the formData variable, which, again, is all the data from the object. The second argument when using the spread operator is going to be the change that's occurring on that input field, the key of the input which is the targeted or updated change that's affecting the state. So here we're going to include e.target.value as the value in the key value pair.


Showing the difference between separate handlers for each state and condensing into one handler for formData state


This is already looking much nicer, but wait, this is only updating the username. What we need to do is update the other fields by finding a way to use the other keys in the object. We need a way to dynamically determine which part of the form we want to update. Luckily for us, the inputs actually have ids assigned to them that match exactly the name of the input fields we want to target, it matches the keys of our data object which is great for us! Maybe something to keep in mind when initially creating the form. It also does not necessarily mean you need to use id all the time, you can also use name if it has one or if you created one within each label. So here we go, let's use the ids for now.

Let's create a variable in the new handleChange function to specify each targeted field by id. This will target which key in state we want to update. If we want to use the variable key dynamically to determine which key in the form we're updating, we can write it by wrapping it in bracket notation and have the value of that key pair be the event grabbing the value on change. That will look like this:


Creating a new variable to dynamically capture the key value pair and show where the code changes


However, there's one more thing that's not quite working anymore due to all our changes and it's the last field, our special checkbox. It needs particular logic in order to show true or false when checked or not instead of logging as just on. We can even test this using our Components extension in our DevTools by checking the true or false state we specified in our data object when we click the checkbox. But we still need to figure out some way of differentiating when we click on the check box that this kind of input is separate from all our other inputs. Let's add a new variable for that value so that every time the target event type is a checkbox, show checked, and if it's not a checkbox type, then return the other kinds of input values. This is where a ternary operator will come in handy to render this logic.


Showing the change in code when adding the new variable of the logic needed to change state for checkbox


Beautiful. Now the fun part. Delete all those old handlers. Clean house, my friend! And now, update all the old onChange handlers and replace them with the ONE handleChange function we created. It will now be easier to add a new field if needed to our form by simply adding it to the data object and just including it in the JSX with the already created logic to render user changes.

Final Code


import React, { useState } from "react";

function Form() {

  const [formData, setFormData] = useState({
            username: "",
            password: "",
            avatar: "",
            mailingList: false
  })

    function handleChange(event) {
        const key = event.target.id
        const value = event.target.type === "checkbox" 
        ? event.target.checked : event.target.value

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

  return (
    <form onSubmit={handleSubmit}>
      <h1>Create an Account</h1>
      <label htmlFor="username">Username</label>
      <input
        type="text"
        id="username"
        value={formData.username}
        onChange={handleChange}
      />

      <label htmlFor="password">Password</label>
      <input
        type="password"
        id="password"
        value={formData.password}
        onChange={handleChange}
      />

      <label htmlFor="avatar">Avatar Image</label>
      <input
        type="text"
        id="avatar"
        value={formData.avatar}
        onChange={handleChange}
      />
      <img src="https://i.pinimg.com/originals/0a/dd/87/0add874e1ea0676c4365b2dd7ddd32e3.jpg"
        alt="Avatar preview"
      />

      <label>
        Join our Mailing List!
        <input
          type="checkbox"
          id="mailingList"
          checked={formData.mailingList}
          onChange={handleChange}
        />

      </label>
      <input type="submit" value="Sign Up" />
    </form>

  )
}

export default Form
Enter fullscreen mode Exit fullscreen mode

So succinct, no?

Bonus: Adding a Post Request

With every proper form, we would want to capture and store new user data somewhere, right? Well, we have our form ready to accept a POST request to it. We just need to add it to the handleForm function so that any new user input gets captured and added to our database. Here's a simple example:


const handleForm = (e) => {
    e.preventDefault()

      fetch("http://localhost:3000", {
        method: "POST",
        headers: {
          "Content-Type" : "application/json"
         },
        body: JSON.stringify(formData)
        })
         .then(res => res.json())
         .then(data => setNewFormData(data))

      setFormData({
            username: "",
             password: "",
            avatar: "",
            mailingList: false
      })
    } 
  }
Enter fullscreen mode Exit fullscreen mode

The second promise will include the function that will handle the new state taking in the new data that we're adding to the database. That will be up to you to decide where you want that function to live and whether you'll need to pass it down as a prop to your form component, but for now, this is the boilerplate POST request you can add on to your form.

If you notice that the input fields aren't resetting once you submit new data, then look to reset the state back to initial object state that holds the empty strings and false value once the request has been completed.

Now go out there and make magic happen by trying it out yourself! ✨

Resources:
Visual Representation

Top comments (0)