DEV Community

Cover image for I was creating Forms the wrong way all along in React.js 🤔
Kuvam Bhardwaj
Kuvam Bhardwaj

Posted on

I was creating Forms the wrong way all along in React.js 🤔

Introduction

When I was creating a signup form, I found myself creating dozens of useStates & then creating dozens of onChange handlers for those useStates. Something like this 🤮

Man I feel sleepy even writing this for illustration!

Also, you're welcome, for blasting your eyes with that monstrous One Light theme code snippet. Some amount of white is good for attention aye! 😉

So... you get the point, In this post, I'll be trying to solve this problem in an elegant manner (certainly not the BS I did in my previous post, making a buggy React Form component which no one even bothers to have a look at!)

Let's Get Started!

Code

export default function App() {

  // NOT a even a SINGLE useState babyyyyyyy! 😎

  const submitForm = (e) => {
    e.preventDefault();

    const formData = new FormData(e.target);
    const inputObject = Object.fromEntries(formData); // convert the FormData object to a JSON object

    console.log(inputObject);
  };

  return (
    <div className="App">
      <form onSubmit={submitForm}>
        <div>
          <input name="email" placeholder="email" />
        </div>

        <div>
          <input name="password" placeholder="password" />
        </div>

        <div>
          <input name="phone" placeholder="phone" />
        </div>

        <br />

        <button type="submit">Submit</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

You can try this code & tinker with it in the same browser as you're viewing this post, thanks to codesandbox (code link)

For my beginner friends, who are new to React, what we did here was:

  • wrap the input fields in an actual HTML form element
  • define the name attribute of each of the input fields (can be anything, HTML uses this attribute to name the input value against it)
  • create a button with no onClick handler but a type attribute set to 'submit'
  • define an onSubmit handler under the form element

After the user has done typing their details in the input, clicking on the button with type='submit' declared in the form, will cause the HTML form element to call its onSubmit handler i.e our submitForm function.

const submitForm = (e) => {
  // 1
  e.preventDefault();

  // 2
  const formData = new FormData(e.target);

  // 3
  const inputObject = Object.fromEntries(formData); // convert the FormData object to a JSON object
  console.log(inputObject);
};
Enter fullscreen mode Exit fullscreen mode

Now, we've done 3 things here:

  1. call the preventDefault method of the HTML FormEvent type, passed as an argument into our function by the HTML Goddess herself (we named it e). This function prevents the form from continuing its default behaviour after submission which includes automatically making a GET request to the same page's URL with the form input values as payload AND reloading the page (we don't want that because we're defining our own form submit logic whose functioning will be interrupted by a page reload)

  2. Pass the form element (referenced as e.target) in a FormData constructor & store it in our formData constant.
    This will take the input elements and parse them into key-value pairs where the key is what we defined in the name attribute against our inputs & the value will be their corresponding input text. All the different input text values can be retrieved using their name, something like this:

// quite similar to the map syntax to get a key ( map.get("key") )
const email = formData.get("email")
Enter fullscreen mode Exit fullscreen mode

Isn't that quite magical AND elegant? HTML does all the work for you from parsing the form input values to collecting the data & returning it in a map-like structure 🪄.=

3.Last but not least, we convert the FormData object which has our input values, to a plain JavaScript object with Object.fromEntries( ... ). Logging the, now created object, gives this output:

IT WORKS!

BUT! and that's a big but (pun intended), the cons of this approach is that you CAN'T write Controlled Inputs. For that, you HAVE TO declare a useState & It's corresponding onChange handler.

"What the hell is a controlled Input?"

Have a look at this example

This is a controlled input in React js, but for inputs like this, we can use the hybrid approach:

  • Define all the inputs in a form
  • Write useState & onChange handler ONLY for those inputs that are controlled
  • Then, manually set values in the formData to those controlled state variables

P.S:
Seeing HTML automatically handling inputs for us appears to be some kind of magic and to be honest, I hate when magic is occurring BUT some amount of magic is bearable for the sake of making the developer experience good 👌🏻


Aaaand... that's a wrap!
Like this post, if you liked it 🙃

But if you loved it? man you gotta follow me on Twitter 😉

Bye for now!


Discussion (57)

Collapse
joelbonetr profile image
JoelBonetR

That's it!
It was really hard on my last projects to make junior devs to understand that, because if you only learnt react that's what you'll find in the doc about managed/controlled forms so React is the only "source of truth".

That's useful on multi-step forms but I still can't find a way in which this is better than using the DOM itself through HTML.

Moreover you can set validations as well in the HTML using the pattern attribute and use :valid and :invalid CSS selectors to style them depending on the state they're in (I thought I made a post on that stuff but I just realized I didn't so maybe I should cover all that stuff in a single place 😂).

Collapse
kuvambhardwaj profile image
Kuvam Bhardwaj Author

Yeah, that appears quite a large bite for a junior dev to gulp 😂
Thanks for the comment btw ✨

Collapse
joelbonetr profile image
JoelBonetR

Yes but just because some years ago people learnt HTML to the detail, then CSS, then JS browser API and then frameworks/libs.

I'm finding lately a good amount of juniors that don't know what is the real scope of React... I've even told by one that map is a React method 😅 so my guess is that the issue is the learning path itself...

Thread Thread
kuvambhardwaj profile image
Kuvam Bhardwaj Author

This is a legit issue, and new frameworks like svelte for example, make you learn more of "svelte" and not more of "javascript". Svelte is cool & I love it, but I think one should know fundamentals of Vanilla JS before dipping toes in libraries/frameworks

Thread Thread
joelbonetr profile image
JoelBonetR

Totally agree!

Collapse
starkraving profile image
Mike Ritchie

Thanks for bringing this up! Just because you’re using JavaScript to render the elements doesn’t mean you shouldn’t also render markup that does its own work too… use the tools for what they’re made for!

Collapse
anubha143 profile image
Anubha Pant

Please do write a post and share all the knowledge you have.

Collapse
joelbonetr profile image
JoelBonetR

I do it from time to time, check my profile!
Also follow to receive new posts in your feed 😁

Collapse
k1dv5 profile image
Kidus Adugna • Edited on

Nice way of handling forms. We have to highlight the limitations as well though. You still need value and onChange when you want an input to update when another one is changed. Also if the form rerenders because of another component, your inputs will be cleared (happened to me with a component that shows the current time.) But for simple forms, where it's guaranteed that there will be no rerenders while you fill the form, it can save time.

Collapse
brense profile image
Rense Bakker

Form data is very nice yes :D For the cases where you need to do validation while the user is typing, or otherwise need to have controlled inputs, you can use something like Formik, or put state in a reducer:

const defaultState = {
  firstname: "",
  lastname: "",
  someOtherFormField: ""
}

function onChange(fieldName:string, value:string) {
  return {
    type: "CHANGE_FIELD" as "CHANGE_FIELD",
    payload: { fieldName, value }
  };
}

const reducerActions = [onChange] as const;

function myFormReducer(
  state: typeof defaultState,
  action: typeof reducerActions[number]
){
  switch (action.type) {
    case "CHANGE_FIELD":
      return { ...state, [payload.fieldName], payload.value }
    default:
      throw new Error();
  }
}

function myFormComponent(){
  const [{ firstname, lastname, someOtherFormField }, dispatch] = useReducer(myFormReducer, defaultState)
  return <form>
    <input value={firstname} onChange={e => dispatch(onChange('firstname', e.target.value))} />
    <input value={lastname} onChange={e => dispatch(onChange('lastname', e.target.value))} />
    <input value={someOtherFormField} onChange={e => dispatch(onChange('someOtherFormField', e.target.value))} />
  </form>
}
Enter fullscreen mode Exit fullscreen mode
Collapse
daaitch profile image
Philipp Renoth

Thanks for sharing, cool article. Especially the new FormData(e.target) part make things more easy.

My opinion: don't use "controlled components". If you have them, remove them and use the approach <form onSubmit={..}>. If you need validation, you can check input in onChange and use like setUsernameError(..) if you need to really render the component to show an error, but not every keyboard hit will make it render.
At least there are some libs for forms.

Collapse
lukeshiru profile image
Luke Shiru

If you need validations, you can simply use native validations (required, pattern and so on) which are accessible by default. If you want to customize the look, you can use checkValidity and the invalid event. No need for state for that either :D

Collapse
kuvambhardwaj profile image
Kuvam Bhardwaj Author

Thanks for the comment, Philipp :)

Yes, you're right, and I personally use the same approach too, but I have seen in codebases, sometimes people like to use this
value{ x } onChange={e => setX(e.target.value)}

Thought might include it as well!

Collapse
link2twenty profile image
Andrew Bone

Very cool, a much better way to interact with forms and save on that precious memory.

If you're going to be using this method a few times it's worth having a little hook with common functions built in, rather than writing them over and over.

Collapse
kuvambhardwaj profile image
Kuvam Bhardwaj Author

Thanks for the code,
Great idea to abstract the logic into a hook 🙌🏻

Collapse
spotnick profile image
spotnick

Very cool article! I did the same mistake in my early stages of react, generating a state for every property which I needed in my form :). Can you enlighten me one more. I used react-hook-form in the past. For controlled inputs I think it's unbeatable but would you still use the native html approach?

Collapse
kuvambhardwaj profile image
Kuvam Bhardwaj Author

Man do what suits you best, Personally for me, I like to minimise package dependencies, thus would use the native HTML approach until it takes a lot of work to roll with it, then I would look for other alternatives as needed :)

Collapse
spotnick profile image
spotnick

Sounds legit. Not depending on a lib is always good. Can you do also dependant validation? Lets say if a=dog, b can only be "good Boy | bad Boy" ? Otherwise it has no validation.. smth like that? Is that possible with native HTML validators? I'm not experienced with that sorry if that sounds kinda stupid

Thread Thread
kuvambhardwaj profile image
Kuvam Bhardwaj Author

Dont be sorry for asking stupid questions, NEVER.
Coming to HTML-native way of validating, HTML can check for empty fields but complex validation? no, you have to implement that yourself in the onSubmit handler.

See this code : codesandbox.io/s/trusting-torvalds...
Try submitting the form without entering any value ;)

Thread Thread
spotnick profile image
spotnick

Thanks. Yeah I was aware of "simple" validation. Ok if I need something more complex I will continue using react-hook-form :)

Collapse
noriller profile image
Bruno Noriller

Not sure you've used this, but you can control them all in a single state with something like this:

handleChange = (e) => {
  setState((old) => ({...old, [e.target.name]: e.target.value}))
}
Enter fullscreen mode Exit fullscreen mode
Collapse
dank1368 profile image
DanK1368

What about handling forms like this?

const [values, setValues] = useState({
first_name: "",
last_name: "",
email: "",
})

const handleInputValues = (e) => {
    setValues(prevState => {
       return { ...prevState, [e.target.name]: e.target.value }
    })
}

<input type="text" name="first_name" onChange={handleInputValues}/>
<input type="text" name="last_name" onChange={handleInputValues}/>
<input type="text" name="email" onChange={handleInputValues} />
Enter fullscreen mode Exit fullscreen mode

I've been using this method for some time now, without any issues

Collapse
brent_charlton_93634e6ae0 profile image
Brent Charlton

Using react native - I ended up going down this route myself naturally but I actually found that having the complexities that this brings also hindered me quite a bit from actually creating quickly.
The reusability is helpful but I feel like it makes code harder to read somewhat

Collapse
hayk94 profile image
Hayk Safaryan

This is quick and neat for very small or pet projects, but please use formik or react-hook-form for any real client production apps

Collapse
jcyber101 profile image
_jcyber

Is it safe to say that generating multiple useStates for a form can cause a performance issue? Also I wonder why if there are better ways to handle input why don’t they teach that in bootcamps? That’s frustrating

Collapse
karlkras profile image
Karl Krasnowsky

Yeah, I've gone through the controlled form element exercise a few times but have decided using default html5 form validation and element types with some mods where needed does what I need without all of the event handling and state management overhead .
I'm personally annoyed by validations that pop up on element blurs, etc., and am perfectly happy with validation performed on submit. Standard pattern validation can provide most, if not all, of the oddball input requirement conditions, and overriding validation messages where needed to be more informative when needed.
Besides, IMO, this behavior is more expected. "Realtime" validation is unnecessarily noisy and can be clunky. Oh, and adds support overhead.

Collapse
psypher1 profile image
James 'Dante' Midzi

Wait wait wait... Huh?

What is this sorcery? 😁

Collapse
kuvambhardwaj profile image
Kuvam Bhardwaj Author

This sorcery is also called HTML ✨

Collapse
psypher1 profile image
James 'Dante' Midzi

Ah yes, the most darkest of dark arts - HTML 😄

Collapse
ivanjeremic profile image
Ivan Jeremic

react-hook-form is close to this vanilla approach and takes care of all edge cases, it us the best form lib out there for react.

Collapse
geobrodas profile image
Georgey

I use react hook forms, data validation + performance + state handling all in one, and bundle size is optimum as well.

Collapse
umair_butt_4af091ffea94b9 profile image
Umair Butt

A good intro to the inner workings of forms. But yeah, when it comes to forms, don’t do it yourself is my recommendation! Formik is one of the best libraries I’ve worked with and would highly recommend for anything form related.

Collapse
lathifahdhiya profile image
Lathifahdhiya

Thank you for this! I never thought that I can abuse html form tag for submitting forms.

Collapse
hyggedev profile image
Chris Hansen

I like this approach! Thanks for sharing. I graduated from a bunch of useStates, to using [e.target.name] approach, but this is far superior 💯 Can't wait to dive into it.

Collapse
maxim_colesnic profile image
Maxim Colesnic

This is what can be solved in Vue in one line
<input type=“email” v-model=“formData.email” /> ?

Collapse
fayomihorace profile image
Horace FAYOMI

Cool article, is there a way to do custom field validations on typing using that form DATA ?

Collapse
m3cv1no profile image
Majd Abu Hmoud

Noice 😎🔥

Collapse
manjit2003 profile image
Manjit Pardeshi

Try something more better and extensible like Formik or Hook form

Collapse
francoisaudic profile image
francoisaudic

The form fields lack labels in order to be compliant to WCAG 2.1 : w3.org/TR/WCAG21/#name-role-value

Collapse
sashevuchkov profile image
Sashe V.

Yeah! That's the proper way to handle forms without third-party library

Collapse
iabhishekrajpoot profile image
iabhishekrajpoot

That's awesome ☺️

Collapse
wilmela profile image
Wilmela

I actually used this on my last project. Thanks for throwing more light.

Collapse
damkols profile image
Kolapo Damola Usman

Thanks for sharing this Kuvam, saving us from writing so many useState code

Collapse
lamarcke profile image
Lamarcke

Nice article. I highly suggest taking a look at Formik and similar libraries, it seems like that's what the cool kids are using nowadays.

Collapse
kuvambhardwaj profile image
Kuvam Bhardwaj Author

I kinda wanted to take an HTML native approach here :)
Ofcourse, if you've got a complex use-case, you'd go for formik or any other form handling library,

Thanks for the comment btw 🙌🏻

Collapse
webjoyable profile image
webjoyable

I understand this is just an HTML approach, but I encourage you to use libraries such as React Hook Form and Formik. Benefits are incomparable

Collapse
kuvambhardwaj profile image
Kuvam Bhardwaj Author

Agreed 🙌

Collapse
geforcesong profile image
George Guo

very good solution