Hook into complex form state using React

Aditya Loshali
Aug 20 · 5 min read

an alternative to some goodness of this.setState()

is everybody hooked yet ?

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 and several different types of 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 your 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 current.

const [modalActive, updateModal] = useState(false)
.
.
.
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('')
.
.
.

so, our other option would be, useReducer

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. although the reducer function 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.

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 utilizing, if the updateArg is a callback 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. you just call updateState with a new object with a piece of the state that you want to update and it will merge it with old one and return new state.

2- The object has the _path and _value properties — when the update 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 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)

but the set method mutates the object inplace and does not return a new copy, but in react world change detection depends on immutability, a fresh new copy of data.

so to bypass this we use immer which helps with handling immutability with javascript objects in a easy to use form.

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 in the draft state, is done on the copy. 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 callback function too. well, that’s not declarative enough !!!. I’d use useEffect for that.

Aditya Loshali

Written by

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

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade