DEV Community

Sydney Andre
Sydney Andre

Posted on • Edited on

React's useReducer Hook

I am working on creating a customer intake form for a local bicycle repair shop. The form includes sections where the customer can input their contact information, details about the bike, and check off which services they need to schedule.

The primary goal is to ensure all of the components I create are as reusable and customizable as possible. In this post I will explain how I set up this form so that there is no need to make any changes to the components if a question on the form needs to be updated or added. The only aspect that will need to be updated is the data imported into the components.

While planning, I determined the contact and bike information would need text inputs, and the services would need checkbox inputs. Because the checkbox input would produce a boolean value and the text would produce a string, their event handling functionality would be different, so I planned to create a component for each input type.

I realized there would be many state changes occurring through events in the child components, but I needed state to be managed at the parent most component. At first, I was not exactly sure how to handle this, but after some research I found the React hook, useReducer.

This hook allows you to add a reducer function to your component which helps to consolidate the state update logic outside of your component in a single function. Just like when initializing useState, you would call the hook at the top level of your parent component. In this example, that is Form.jsx. useReducer takes in 2 parameters: a reducer function and the initial state of the component. It returns the updated state and a dispatch function that uses the reducer to make updates to the state.

export const Parent = () => {
const [state, dispatch] = useReducer(reducer, initialState)
...
}
Enter fullscreen mode Exit fullscreen mode

useReducer Arguments

Let’s look a little closer at what this reducer argument is made up of. The reducer function takes in the current state and an action object. The action object typically has a property named ‘type’ that is used to define the action that is taken on the state. Additionally, the action object can have additional properties. In the example below, it also has a ‘key’ property and a newValue property.

The reducer function is often made up of a switch case that is dependent on the type property on the action object. Based on the type that is being passed in, a defined action is taken on the state. In the example below, if the type is ‘add_text’, then the state is returned with a new value at the key defined by the action object.

The reducer function can be defined within your component or outside and imported in. In this example it is defined as follows:

const reducer = (state, action) => {
   switch(action.type) {
       case 'add_text': {
           return {
               ...state,
               [action.key]: action.newValue
           }
       }
       case 'change_boolean': {
           return {
               ...state,
               [action.key]: !state[action.key]
           }
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

In this example the initial state is an object named allQuestions which is defined outside of the component and imported in. It is in the exact shape the backend expects so that all of the data points can be added to the database on submission.

What useReducer Returns

Now that we understand what arguments need to be passed into the useReducer, let’s look at what the hook returns and how it is used to help manage the state.

useReducer returns the dispatch function which allows you to update state and trigger a rerender. An action object is the only argument that needs to be passed into the dispatch function. This object will include the properties that were used in the reducer function, for example:

dispatch({
       type:'add_text',
       key: someKeyName,
       newValue: someNewValueName
   })

Enter fullscreen mode Exit fullscreen mode

This would be similar to the setState function that is returned when using useState. Also similar to useState, the new state with the appropriate updates made is returned when dispatch is called.

In Action

Now let’s see this in action! First, I defined two event handling functions that wrap the dispatch function. The reason for this is to be able to pass in values to the action object. This is important for reusability. This way I can pass in the state property that needs to be updated, as well as, the new value of that property when the type is ‘add_text’

const handleAddedText = (e, key) => {
   dispatch({
       type:'add_text',
       key: key,
       newValue: e.target.value
   })
}


const handleCheckBoxChange = (key) => {
   dispatch({
       type:'change_boolean',
       key: key
   })
}
Enter fullscreen mode Exit fullscreen mode

These functions are then passed down to TextInputList.jsx and CheckboxInputList.jsx from From.jsx.

To manage the questions that will make up the form, I created an array of objects with the necessary question information for each input. I imported the respective question data arrays into each List component to map through and return the Input component which takes the event handler function passed down originally from Form.jsx, as well as the question data. Let’s look at the CheckboxInputList component for example:


import React from 'react';
import { CheckBoxInput } from './checkboxInput';
import { checkBoxLabels as cbLabelObjArr }  from '../formQuestionData';

export const CheckBoxForm = ({ handleCheckBoxChange }) => {
   return (
       <div>{cbLabelObjArr.map((obj, i) => (
           <CheckBoxInput
               key={i}
               label={obj.label}
               dbName={obj.dbName}
               handleCheckBoxChange={handleCheckBoxChange}
           />
       ))}</div>
   )
}
Enter fullscreen mode Exit fullscreen mode

Now let’s check out how the CheckBoxInput component is set up:

import React from 'react';

export const CheckBoxInput = ({ key, label, dbName, handleCheckBoxChange }) => {

return (
       <div >
           <div>{label}</div>
           <input
               type='checkbox'
               onChange={() => handleCheckBoxChange(dbName)}
           />
       </div>
   )
}
Enter fullscreen mode Exit fullscreen mode

And that's it! Now, we have successfully utilized the useReducer hook to organize event handling across multiple components, and created highly flexible components. Throughout this process I became very familiar and confident in using the useReducer hook and created highly reusable components that will ensure the client has flexibility to update their form as needed.

For more information and details check out React's useReducer documentation.

Top comments (0)