DEV Community

WebDevZTH
WebDevZTH

Posted on

Creating a custom hook in React to control form input

There are numerous ways to make a form input controlled from not controlled. The internet is mostly just a collection of forms ^_^ and one solution to make interacting with form inputs is to use the following approach...

Suppose we have a form with a single input field

<form>
<input type='text' id='username'/>
<button type='submit'>Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

In the above example we are sending the field 'username' to our backend or to some end-point on submitting this form. To do this we must have two things a 'onSubmit' event and a way to keep track of the data entered into the input field of the form, one approach is...

const [value, setValue] = useState('')

<form onSubmit={handleFormSubmit}>
<input value={value} onChange={({target}) => setValue(target.value)} type='text' id='username'/>
<button type='submit'>Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

The above snippet is making use of the 'useState' hook to make this component stateful. We are then passing the value and onChange event handler to the input field to keep track and store the value enetered into 'username'

This approach works fine and good but we will have to define multiple 'onChange' event handlers if our form gets more input fields and as the complexity grows the components code will start to look messy.

One thing we can use here is custom hooks, that is we create a function that utilises the react-hooks and customise its behaviour to suit our needs. One approach to do this is like so...


import {useState} from 'react'

export const useField = (type) => {
 const [value, setValue] = useState('')
 const onChange = (event) => {
  setValue(event.target.value)
 }
 const reset = () => {
  setValue('')
 }

 return {
  type,
  value,
  onChange,
  reset
 }
}
Enter fullscreen mode Exit fullscreen mode

Now we can import this custom hook into any component where we want to use input fields, like so...

import useField from '..file its being exported from'
Enter fullscreen mode Exit fullscreen mode

Then we can use it in our react component, like so..

const {reset, ...username} = useField('text')
Enter fullscreen mode Exit fullscreen mode
<form onSubmit={handleFormSubmit}>
<input {...username} type='text' id='username'/>
<button type='submit'>Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Now one way to implement the handleFormSubmit is like so...

const handleFormSubmit = (event) => {
 event.preventDefault()
 let userObject = {
  username: username.value
 }
 // send data to backend
 reset()
}
Enter fullscreen mode Exit fullscreen mode

We can call the reset method after successfully sending form-data to the backend and the input field is reset to an empty string.

Using this approach we make the code much cleaner and dont need to use multiple useState hooks within our components which clutters the code.

Try this in your next form in React! ^_^

Top comments (10)

Collapse
 
leandroreschke profile image
Leandro Reschke • Edited

So, if I needed another input I would just do this? {reset, ...username, ...age} = useField('text').

I didn't get it, how would I avoid multiple useStates with this.

Sorry, I'm new to React, coming from Angular and VueJS.

Great article BTW. Thanks

Collapse
 
leob profile image
leob • Edited

Well it's not just you, I also fail to grasp how this works. The idea seems nice, but the author could have elaborated just a tiny little bit more for the mere mortals among us.

Ah on reading it a second time I get it now ... for a second field (let's say "email") you'd just do:

const {reset, ...email} = useField('text')
Enter fullscreen mode Exit fullscreen mode

so for those 2 fields you'd have (we need to "rename" the 'reset' member during destructuring):

const {reset: resetUsername, ...username} = useField('text')
const {reset: resetEmail, ...email} = useField('text')
...
<input {...username} type='text' id='username'/>
<input {...email} type='text' id='email'/>
Enter fullscreen mode Exit fullscreen mode

and so on ... the only thing I don't get is why we need the "type" parameter to the hook: useField('text') ... and we probably also don't need the "id" attributes (id='username' and id='email').

So, this might work equally well (and save some typing) ... ?

const {reset: resetUsername, ...username} = useField()
const {reset: resetEmail, ...email} = useField()
...
<input {...username} type='text'/>
<input {...email} type='text'/>
Enter fullscreen mode Exit fullscreen mode

Other than that, yeah pretty clever and elegant.

Collapse
 
leandroreschke profile image
Leandro Reschke • Edited

I came to the same conclusion, but if this is true, then now I have multiple useFields instead of useStates, it is just omitting value and onChange. About the Type parameter, I think he wanted to also set the input type.

Thread Thread
 
leob profile image
leob

Yes you'd have multiple useFields, I think that's clearly the idea.

But it's not omitting value and onChange - instead, value and onChange are being expanded as props on the elements, so you're effectively linking the attributes on the input elements with function closures which are generated by the "useField" calls.

I still don't see the point of the type param, unless you'd end up with just:

<input {...email}/>
Enter fullscreen mode Exit fullscreen mode

where the 'type' attribute is then also generated through the object expansion (the ... ellipsis).

Collapse
 
webzth profile image
WebDevZTH

Yeah absolutely, we need not have multiple onChange handlers either. The type parameter is for the input element's type attribute is required to mention what type of input it is, could be email, password, text, number, range etc.

Collapse
 
alitnp profile image
Ali Hamedani

So instead of repeated useStates we have repeated hook definings,
What ever you do, forms are painfull in react,
The most clean approach is to put all of elements in a single useState object, and access them by object notation like 'form[e.target.name] = e.target.value' but with the cost of rerendering the whole form in every input value change. I actually do it if there's only 2 or 3 inputs.

Collapse
 
leob profile image
leob

Clever and elegant technique, love it, but why does the hook need a "type" parameter? Looking at the code I have no clue what's that used for TBH.

Collapse
 
ausmurp profile image
Austin Murphy

You could use the type param to derive the value, as not all input types will get value from target. Also allows you to just spread the rest into the input props with no additions.

Collapse
 
ausmurp profile image
Austin Murphy

I like this simple elegant solution. I'm a react-hook-form fan myself but this is great if you don't need anything too complex.

Collapse
 
leob profile image
leob

Ah right, got it ... yes then I see how 'type' could be useful.