Complex form state using React Hooks

An alternative to the goodness of this.setState()

Aditya Loshali
Sep 17 · 5 min read
is every body hooked yet ? Image Credits — Free Code Camp

So, it’s been quite some time since React hooks were released. and from the looks of it, everybody is going gaga over them. Well, i understand. because i’m one of you too. Hooks got me hooked!

Hooks allow us to create smaller, composable, reusable more manageable React components.

Sometimes you may be using hooks to manage the form state, using useState or useReducer.

Now, let’s consider a scenario where you have to manage a complex form state with multiple form inputs, which can be several different types like text, number, date input. The form state may even have nested information for example a user’s address information which has it’s own sub-fields like address.addressLine1, address.addressLine2 etc.

Maybe you also have to update form state based on the current state, like a toggle button.

Now, if you are using useState for each individual form field, then you get the ability to compute new state based on the current state.

const [modalActive, updateModal] = useState(false)
.
.
.
// new state based on previous
updateModal(prev => !prev)

but, if you have too many individual form fields, like 100+, ( YESS!!. I was managing 100+ form fields ) then this approach isn’t friendly.

imagine !!..

const [firstName, setFirstName] = useState('')
const [middleName, setMiddleName] = useState('')
const [lastName, setLastName] = useState('')
.
.
.
.

It isn’t practical to write different useState and then use separate update function for each field.

So, our other option would be the Hook, useReducer.

Let’s look at an example.

const initialState = {
firstName: '',
lastName: ''
};
function reducer(state, action) {
switch (action.type) {
case 'firstName':
return { firstName: action.payload };
case 'lastName':
return { lastName: action.payload };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<input
type="text"
name="firstName"
placeholder="First Name"
onChange={(event) => {
dispatch({
type: 'firstName',
payload: event.target.value
})
}}
value={state.firstName} />
<input
type="text"
name="lastName"
placeholder="Last Name"
onChange={(event) => {
dispatch({
type: 'lastName',
payload: event.target.value
})
}}
value={state.lastName} />
</>
);
}

Eh !!, not good.

You cannot possibly write, each use case for those n number of form fields in the reducer.

However, the reducer function used in useReducer is just a normal function that returns updated state object. so, we can make it better.

function reducer(state, action) {
// field name and value are retrieved from event.target
const { name, value } = action

// merge the old and new state
return { ...state, [name]: value }
}

Now this looks a better and cleaner reducer.

But … this does not allow us to compute new state based on current state while calling update function with a callback. like we can do with..

this.setState((prev) => ({ isActive: !prev }))// orconst [modalActive, updateModal] = useState(false)
.
.
.
updateModal(prev => !prev)

also, what about updating nested state like address.addressLine1, address.pinCode.

Ok !!. we’ve had lot of discussion regarding managing complex form state by using not so ideal approaches.

Let me just show you the solution.

ta da!!

So, here’s the full source code for handing such complex form scenarios.

I’ll explain the reducer ( enhancedReducer :P ) function a little.

The reducer function receives two arguments, the first argument is the current state before the update. This argument is automatically provided when you call the updateState / dispatch function to update the reducer state. The second argument of the reducer function is the value you call the updateState function with, it need not be the typical redux action object of form { type: ‘something’, payload: ‘something’ }. It can be anything, a number, string, object or a function even.

And this is what we are utilising. If the updateArg is a function we call it with the current state to calculate the new one. Whatever object we return from this function becomes our new state.

And if the updateArg is an plain old Javascript object, then there are two cases.

1- The object does not have the _path and _value properties — and thus is a normal update object just like we give to this.setState. So, you can just call updateState with a new object with the pieces of the state that you want to update and it will merge it with old one, and return the new state.

2- The object has the _path and _value properties — when the updateState function is called with an object with these two properties. we treat this a special case where _path represents a nested field path. In either a string form eg: ‘address.pinCode’ or an array representing the path [‘address’, ‘pinCode’].

But, what do we do with such path representations to update a nested field in an object?. We use lodash’s set method. It accepts both of the path form as valid inputs to update and object.

set(objectToUpdate, path, newValue)const state = {
name: {
first: '',
middle: '',
last: ''
}
}
// and to update, for eg: first name.
// both ways of path are correct.
set(state, 'name.first', 'Aditya')
set(state, ['name', 'first'], 'Aditya')

But, the set method mutates the object in-place and does not return a new copy, but in the React world change detection depends on Immutability, a fresh new copy of data, with a new location in memory.

So, to bypass this we use immer, which helps with handling immutability with Javascript objects in a easy to use form.

import produce from 'immer'produce(state, draft => {
set(draft, _path, _value);
});

produce function from immer takes the object to work on as it’s first argument which in our case is the current state, and it’s second argument is a function which receives a draft copy of the object to mutate, whatever you modify inside of this function on the draft state, is done on the copy, not the actual input object state in-place. and then, it automatically returns the new object with updated data.

So, there’s our enhanced reducer :D

Just

yarn add lodash immer

and enjoy.

PS- The example in the gist can be refined much further, with more edge cases handled in enhancedReducer and the form fields code can be shortened by mapping over the form spec object to create it dynamically and reduce code duplication and some other things too.

Some of the readers may feel otherwise about this approach. So, we can always discuss over it in threads.

Maybe some of you may have a question that if we are so trying to replicate this.setState then why not have the setState second argument callback function too, to perform some action after the state has been updated. Well, that’s not declarative enough! We will be using the approach of — first do this, then do that. We will be telling the code, step by step, how to do something. Instead of simply telling it what to do. I’d use useEffect instead of callback function and the how to do approach, because that’s declarative, reacting to changes.

Declarative vs imperative, and functional programming is a whole another talk, that I’ll share in future.

Resources —

JavaScript in Plain English

Learn the web's most important programming language.

Aditya Loshali

Written by

I’m a full stack web developer working with Javascript on React and Node.js everyday.

JavaScript in Plain English

Learn the web's most important programming language.