DEV Community

Stanley Jovel
Stanley Jovel

Posted on

Simplify controlled components with React hooks

While working with react it is almost inevitable to come across controlled components. A controlled component is a react component that controls the values of input elements in a form using setState().

Before the new hooks API was introduced, you could only use class components for this purpose since they are the only ones that can store state and have access to the setState API. But now with the introduction of hooks, we can finally handle state changes in any component (functional or class) greatly simplifying writing controlled components.

Here is an example of a controlled component using the traditional approach with a class component:

RegularControlledComponent.js

import React, { Component } from 'react'

export class RegularControlledComponent extends Component {
  state = {
    username: '',
    password: '',
  }

  handleUsernameChange = (e) => this.setState({
    username: e.currentTarget.value,
  })

  handlePasswordChange = (e) => this.setState({
    password: e.currentTarget.value,
  })

  render() {
    return (
      <form>
        <div>
          <label>Username:</label>
          <input type="text" onChange={this.handleUsernameChange} />
        </div>
        <div>
          <label>Password:</label>
          <input type="text" onChange={this.handlePasswordChange} />
        </div>
        <input type="submit" />
      </form>
    )
  }
}

At first, it may seem there is nothing wrong with it, but what would happen if instead of two input fields we had 5 or 10? we will need 10 handleSomeInputFieldChange function handlers.
THIS APPROACH IS NOT SCALABLE

Let's rewrite our component to control the input fields using hooks:

ControlledComponentWithHooks.js

import React, { useState } from 'react'

export const ControlledComponentWithHooks = () => {
  const [input, setInput] = useState({})

  const handleInputChange = (e) => setInput({
    ...input,
    [e.currentTarget.name]: e.currentTarget.value
  })

  return (
    <form>
      <div>
        <label>Username:</label>
        <input type="text" name="username" onChange={handleInputChange} />
      </div>
      <div>
        <label>Password:</label>
        <input type="text" name="password" onChange={handleInputChange} />
      </div>
      <input type="submit" />
    </form>
  )
}


The first change to notice is that our component is now a function, with the introduction of the useState hook we are no longer obligated to convert our functional components into class components when we want to use local state.

Secondly, we are now programmatically setting values to our state variables, the way we accomplished this is by adding a new name attribute to the input fields in lines 17 and 25. The magic happens in line 8: [e.currentTarget.name]: e.currentTarget.value
here we are using that name as the property value for our state object and assigning the input value to it.

This approach is scalable since it doesn't matter the number of input fields in this form, they will all use the same handleInputChange and the local state will be updated accordingly. Beautiful

Now! let's make this even better by abstracting the hook into its own file to make it reusable.

useInputChange.js

import { useState } from 'react'

export const useInputChange = () => {
  const [input, setInput] = useState({})

  const handleInputChange = (e) => setInput({
    ...input,
    [e.currentTarget.name]: e.currentTarget.value
  })

  return [input, handleInputChange]
}

Now our functional component ControlledComponentWithHooks.js just have to import and use the new hook.

import React from 'react'
import { useInputChange } from './useInputChange'

export const ControlledComponentWithHooks = () => {
  const [input, handleInputChange] = useInputChange()

  return (
    <form>
      <div>
        <label>Username:</label>
        <input type="text" name="username" onChange={handleInputChange} />
      </div>
      <div>
        <label>Password:</label>
        <input type="text" name="password" onChange={handleInputChange} />
      </div>
      <input type="submit" />
    </form>
  )
}

Isn't it cool? all the setState and input handlers boilerplate have been completely removed from our component. with our new hook creating controlled components is simple as it gets, making our component more readable and focusing on the specific business logic that it was created for.

Conclusion

Just like HOCs and render prop, hooks allow us to reuse logic in our components. We can leverage this to do all sort of abstractions.

All the source code from this article can be found in the following Repl.it:
https://repl.it/@StanleyJovel/Controlled-Components-with-Hooks

Top comments (17)

Collapse
 
dmikester1 profile image
Mike Dodge • Edited

Wow! This is the cleanest and easiest I have seen a controlled input done. I have one question. Every other time I've looked up a tutorial on how to do a controlled input, they have always set the input value to the state variable, e.g.

<input type='text' value='startDate' onchange='changeDate' />

How does this work without requiring the value atribute?

Collapse
 
stanleyjovel profile image
Stanley Jovel

Hello Mike! It should work with or without explicitly setting the value attribute.
Try it yourself by running the source code on repl.it :
repl.it/@StanleyJovel/Controlled-C...

However, the example code you shared doesn't seem to be valid JSX.

Collapse
 
dmikester1 profile image
Mike Dodge

Oh, you mean the missing curly braces? Yeah, I forgot to type those, but they are in my code. What I'm stuck on is how to change those input values from another component? I have lifted the state to a shared parent. But when I add in value={batchDates.startDate} to my input, I get: Warning: A component is changing an uncontrolled input of type date to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.

Thread Thread
 
stanleyjovel profile image
Stanley Jovel

Interesting, Is it okay if I take a look at some of your code?

Thread Thread
 
dmikester1 profile image
Mike Dodge

OK, nevermind, figured it out! :) I wasn't initializing my state variables correctly. But I think in order to be able to set it from a different component, I will need to have that value attribute set to a state value.

Thread Thread
 
stanleyjovel profile image
Stanley Jovel

Awesome 😃

Thread Thread
 
punkah profile image
Orsi

I think the confusion comes from the fact that setting onChange in itself doesn't make the input controlled unless you are setting value as well.

And you are right, the controlled component value needs to be initialized so the input state should be initialized first: const [input, setInput] = useState({ username: "", password: "" }). These initial values could be passed into the custom hook too.

Thread Thread
 
chandra profile image
Chandra Prakash Tiwari

this worked. Thanks Orsi :)

Collapse
 
chandra profile image
Chandra Prakash Tiwari

This is a good approach, but this didn't worked for me.
still showing

index.js:1 Warning: A component is changing an uncontrolled input of type number to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://fb.me/react-controlled-components
Collapse
 
chandra profile image
Chandra Prakash Tiwari

I think you are not using the value of the state object as the value of input..
this is not the controlled input here.

<input type="text" name="username" onChange={handleInputChange} />
Enter fullscreen mode Exit fullscreen mode
Collapse
 
evanlk profile image
Evan Kennedy • Edited

I'd also like to point out that you could also add additional functionality such as 'setDefaults', or 'clearValues' too.

import { useState } from 'react'

export const useInputChange = (defaultValues) => {
  const [input, setInput] = useState({...defaultValues})

  const handleInputChange = (e) => setInput({
    ...input,
    [e.currentTarget.name]: e.currentTarget.value
  })

  const setDefaults = () => {
     setInput({...defaultValues});
  }

  const clearValues = () => {
    setInput({});
  }

  return [input, handleInputChange, clearValues, setDefaults]
}
Collapse
 
ianhancock36 profile image
IanHancock36

Just got out of a bootcamp was struggling with handle changes and hooks etc this makes so much sense and I really like how the hooks is in a separate file which allows a such a clean reading ability thank you for this article!!!

Collapse
 
nikolalakovic85 profile image
Nikola Lakovic

Perfect Stanley! :) Thanks!

Collapse
 
dmikester1 profile image
Mike Dodge

Stanley, wow small world!! I just saw you graduated in Computer Science from UW-Eau Claire. I graduated in Computer Science from UW-Eau Claire as well!!

Collapse
 
stanleyjovel profile image
Stanley Jovel

Really?! That is so awesome!
I did not graduate there though haha I did a 1-year exchange program and absolutely loved it.

Collapse
 
meefox profile image
Ju

"THIS APPROACH IS NOT SCALABLE"
unless this.setState({[event.target.name]: event.target.value})
;)

Collapse
 
stanleyjovel profile image
Stanley Jovel

Yes, that is why I'm using that exact approach