Thinking in Hooks: React Form Management

Rommel Santor
5 min readMar 24, 2019

--

Preface

I’ll just come right out and say it: React hooks are an ingenious innovation that can and should [and I say must] modify the way you approach problem-solving in a React environment.

This post is intended to be part 1 in a series covering the application of React hooks as a solution for some common use cases. In this part, I go over writing and using a hook to achieve controlled form field management. Future installments will include writing a fetch hook for generic API communication, a hook for GraphQL API queries and mutations, populating a form with back-end API data, and submitting a form into a back-end API.

Shallow React Hooks

A rote way of managing form state with React hooks would be to call useState() for each of the fields on a given form. (See this example by Kevin Okeh.) There is nothing wrong with this approach, but it has its limitations including a lack of reusability. For example:

const RoteUserForm = ({ initialFirst, initialLast, initialEmail }) => {
const [firstName, setFirstName] = useState(initialFirst || '')
const [lastName, setLastName] = useState(initialLast || '')
const [email, setEmail] = useState(initialEmail || '')
// ...
return (
<form>
First Name:
<input
onChange={event => setFirstName(event.target.value)}
type="text"
value={firstName}
/>
Last Name:
<input
onChange={event => setLastName(event.target.value)}
type="text"
value={lastName}
/>
Email Address:
<input
onChange={event => setEmail(event.target.value)}
type="email"
value={email}
/>
{/* ... */}
</form>
)
}

Let’s briefly consider some of the obvious issues with a rote approach like this:

1. The get/set for every field must be handled individually.

Yes, it does get the job done, but for me it removes a bit of the elegance hooks offer. You can and should take advantage of the beautiful encapsulation hooks provide. Additionally, handling the native event for each field has a bad smell.

2. Every field on every form requires custom but repetitive, identical logic.

Looking at what the above code is doing, it requires that within every form and for every field you create, you have to define an individual getter and setter then manually call them. It feels very cumbersome; one of the fundamental goals of a developer is to avoid repeating the same code when possible.

3. You cannot introduce other common form or field logic.

In order to add any kind of common logic that should be available to any form or field, you couldn’t readily do that. For example, consider if you wanted to support dirty checking so your application always knows if the form has been modified by the user, given the above component, you would probably have to add it like:

const weAreDirty =
initialFirst !== firstName ||
initialLast !== lastName ||
initialEmail !== email ||
...

Though still not a real solution, a better implementation of ^this dirty logic would be to use hooks, specifically useState and useEffect:

const [weAreDirty, setWeAreDirty] = useState(false)useEffect(() => {
setWeAreDirty(
initialFirst !== firstName ||
initialLast !== lastName ||
initialEmail !== email ||
...
)
}, [firstName, lastName, email])

Indeed, when you start thinking in hooks, lots of different solutions hidden in the wrinkles of your brain start revealing themselves.

Deeper React Hook

Let’s consider how we might want to better approach controlling the field value state in a form. One essential improvement would be to manage all the form values with a single state object, which should live in a custom hook. Let’s call it useFormValues().

Thinking about how our above code could change with a hook, we’d want to consolidate our values into a single object (both for the initial field values and the controlled fields) and simplify our onChange handlers. We can start off with something like:

const UserForm = ({ initialUserData }) => {
const [formValues, formSetters] = useFormValues(initialUserData)
return (
<form>
First Name:
<input
onChange={formSetters.firstName}
value={formValues.firstName}
/>
Last Name:
<input
onChange={formSetters.lastName}
value={formValues.lastName}
/>
Email Address:
<input
onChange={formSetters.email}
value={formValues.email}
/>
{/* ... */}
</form>
)
}

This package is available in the NPM repository under the name react-form-values. You can find the package at npm and the full source at github via the following links:

useFormValues()

If you are just looking for a package to consume in your project, the above should suffice, but I imagine if you’re still reading, you have at least a little interest in the actual useFormValues hook itself, so let’s delve into its inner-workings. (Note that I am excluding dumping the actual source here, but you are welcome to reference it at the GitHub link above.)

Arguments

useFormValues() accepts two parameters:

  1. an object with the initial form fields and values
  2. a callback that will be executed and passed the full form field/value object whenever there is a change in form fields

Return value

useFormValues() returns an array with three elements:

  1. an object with the current form field names mapped to their values
  2. an object with the current form field names mapped to a setter function, allowing you to mutate the form value either directly or as passed to an onChange event handler
  3. a utilities object with the following properties:
  • clearfunction you can call to blank out all known fields
  • isDirtyboolean you can inspect to determine if any fields have been changed from the initial values
  • resetfunction you can call to rollback the form to the initial values

Internals

Internally, useFormValues() uses React’s provided useState() and useEffect() hooks, with three state values and two effects.

useState()

  1. form — the object mapping field names to their current values
  2. initials — the object preserving the initial form fields/values explicitly provided by the user (used for a reset() and for dirty checking withisDirty)
  3. isDirty — the boolean keeping tabs on whether or not the form has diverged at all from the initial values
const [form, setFormValues] = useState({})
const [initials, setInitials] = useState(initialValues || {})
const [isDirty, setIsDirty] = useState(false)

useEffect()

  1. There is one effect that executes whenever the initial values change. It records the new initial values into initials and replaces them into the form object.
  2. The second effect executes whenever the initials object changes or the form object changes, setting the isDirty boolean according to whether or not the form is still matches the initial values.
useEffect(() => {
if (!isEqual(initialValues, initials)) {
setInitials(initialValues)
setFormValues(initialValues)
}
}, [initialValues])
useEffect(() => {
setIsDirty(!isEqual(initials, form))
}, [initials, form])

Logic

The remainder of useFormValues() mainly handles dynamically setting the value for a field name, allowing for an Event value (thus making it eligible as a callback passed to onChange), and creating a dynamic object returned as the second element that will allow users to arbitrarily set a form value whether or not the field name already exists.

Conclusion

React hooks have a world of possibilities to offer in significant power hidden behind a simple, elegant, flexible interface. Things like form management have historically been sloppy to deal with but can be done with a bit more finesse when you start thinking in hooks.

Please feel free to thumbs-up if you didn’t hate this article. (If you did hate it, kudos to Medium for not implementing a thumbs-down button.)

--

--