How To Efficiently Update React State For Multiple DOM Inputs Using the useReducer() Hook

This article assumes some basic familiarity with the useReducer() hook. Examples are using react-bootstrap but you don't need to be using it in your own project for this to work.

Efficient VS Inefficient

Inefficient

Assuming this state object...

const initState = {
    firstName: "",
    lastName: "",
    street: "",
    aptSuite: "",
    city: "",
    stateName: "",
    zipcode: "",
    date: "",
    title: "",
    status: "fillingOutForm",
  };

Assuming a form input element structured like this...

<Form.Label htmlFor="user-first-name">First name</Form.Label>
  <Form.Control
    type="text"
    name="FIRSTNAME" // Used for the action type
    id="user-first-name"
    value={formState.firstName} // formState from useReducer
    required
    onChange={(e) => {
      const name = e.target.name;
      const value = e.target.value;
      dispatch({type: "CHANGE_" + name, payload: value });
    }}
/>

You could have a separate action type within the reducer function for each DOM input such as...

switch (type) {
  case CHANGE_FIRSTNAME:
    // Return modified state.
  case CHANGE_LASTNAME:
    // Return modified state.
  case CHANGE_STREET:
    // Return modified state.
  default:
    return state;
}

This is inefficient however.

Efficient

The solution to this inefficiency is to abstract outwards in the reducer function.

Given this onChange handler...

// For example, the DOM input attribute name is 'firstName'
onChange={(e) => {
  const field = e.target.name;
  const value = e.target.value;

  dispatch({
    type: "CHANGE_INPUT",
    payload: {
      value,
      field,
    },
  });
}}

...the reducer function could contain this...

function formReducer(state, action) {
    const { type, payload } = action;

    switch (type) {
      case "CHANGE_INPUT":
        return { ...state, [payload.field]: payload.value };
      default:
        return state;
    }
  }

Normally one would have more cases in the reducer function but this example is simplified for educational purposes

In the code above, a computed property name is used to take the attribute name of the element ('firstName') and update state in the right place. In this case...

const initState = {
  firstName: "Whatever was type in by user",
  // Rest of state properties...
}

Gotchas

Remember how to access the data needed using computed property names. You need to wrap the dot notation object accessor for the action payload object in brackets.
return { ...state, [payload.field]: payload.value };

Further Cleaning

Optimization of code length can be achieved by moving code from the onChange() handler to its own function. It might even be more descriptive to change the name to something like updateStateWithInputValue.

const changeDOMInput = (e) => {
  const field = e.target.name;
  const value = e.target.value;
  dispatch({
    type: "CHANGE_INPUT",
    payload: {
      value,
      field,
    },
  });
};

onChange={(e) => {
  changeDOMInput(e);
}}

I hope this helps!

Connect With Me!

18