This blog is dedicated to yet another useful React hook “useReducer” in continuation to my previous blog, and some strategies to define state in React components using hooks.

To read some of our interesting blogs on React and React Hooks click here.

Get in touch with our technology experts today for help with your applications and useful recommendations or visit our React Technology page.

The useReducer Hook

The useReducer is a powerful alternative to the useState hook which allows us to initialize state, as well as, encode all the possible actions that can be performed on the state centrally.

The useReducer hook becomes the predictable state container for a component locally. If you are aware of how reducers work in Redux, you can think of the same functionality being rendered by the useReducer hook locally to the component.

We will take an example of a Pizza Toppings picker where the users can select and drop any number of toppings into their recipe bag.

The state would comprise of the ToppingsList which will have the toppings and their count. To keep the example simple, we will initialize the state to a specific set of toppings.

const initialState = { toppings: { Tomato: 0, Onion: 0, Meat: 0, Olive: 0 } }; 1 2 3 const initialState = { toppings : { Tomato : 0 , Onion : 0 , Meat : 0 , Olive : 0 } } ;

Next, we will identify the operations to be performed on the state. In this example, we can add a topping or remove the topping(s) as desired. Also, we have the liberty to reset the toppings to the initial state.

Thus, we will define the reducer function to handle all the 3 operations namely, ADD, REMOVE and RESET. Each reducer action receives an action type with the payment and accordingly, it either returns the value for the modified state or the original state as depicted below.

const toppingListReducer = (state, action) => { let oldCount = 0; let updatedCount = 0; switch (action.type) { case "ADD": oldCount = state.toppings[action.payload]; updatedCount = oldCount + 1; const updatedtoppings = { ...state }; updatedtoppings.toppings[action.payload] = updatedCount; return updatedtoppings; case "RESET": return action.payload; case "REMOVE": oldCount = state.toppings[action.payload]; if (oldCount <= 0) { return state; } updatedCount = oldCount - 1; const updtoppings = { ...state }; updtoppings.toppings[action.payload] = updatedCount; return updtoppings; default: return state; } }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const toppingListReducer = ( state , action ) = > { let oldCount = 0 ; let updatedCount = 0 ; switch ( action . type ) { case "ADD" : oldCount = state . toppings [ action . payload ] ; updatedCount = oldCount + 1 ; const updatedtoppings = { . . . state } ; updatedtoppings . toppings [ action . payload ] = updatedCount ; return updatedtoppings ; case "RESET" : return action . payload ; case "REMOVE" : oldCount = state . toppings [ action . payload ] ; if ( oldCount <= 0 ) { return state ; } updatedCount = oldCount - 1 ; const updtoppings = { . . . state } ; updtoppings . toppings [ action . payload ] = updatedCount ; return updtoppings ; default : return state ; } } ;

Now, we create the hook in our functional component passing the two values illustrated above as arguments i.e. the reducer function and the initial state.

const [toppingList, dispatch] = useReducer(toppingListReducer, initialState); 1 const [ toppingList , dispatch ] = useReducer ( toppingListReducer , initialState ) ;

This returns two variables,

The first (toppingList), is a state that can be used to get access to the state at any point of time.

The second (dispatch), is the reference to the reducer function using which we can dispatch/invoke any action to be handled by the reducer.

We can pass this dispatch method reference as a prop to the child components that could be used to perform state manipulations.

return ( <React.Fragment> <ul className="List"> {Object.keys(toppingList.toppings).map((topping, i) => { return ( <ListControl key={topping} topping={topping} count={toppingList.toppings[topping]} dispatch={dispatch} /> ); })} </ul> <ResetBtn dispatch={dispatch} /> </React.Fragment> ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 return ( < React . Fragment > < ul className = "List" > { Object . keys ( toppingList . toppings ) . map ( ( topping , i ) = > { return ( < ListControl key = { topping } topping = { topping } count = { toppingList . toppings [ topping ] } dispatch = { dispatch } / > ) ; } ) } < / ul > < ResetBtn dispatch = { dispatch } / > < / React . Fragment > ) ;

The child component can perform the dispatch based on the following action.

const listControl = props => { return ( <React.Fragment> <li> {props.topping}: {props.count} </li> <button className="RemoveButton" onClick={() => props.dispatch({ type: "REMOVE", payload: props.topping }) } > Remove </button> <button className="AddButton" onClick={() => props.dispatch({ type: "ADD", payload: props.topping })} > Add </button> </React.Fragment> ); }; export default listControl; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const listControl = props = > { return ( < React . Fragment > < li > { props . topping } : { props . count } < / li > < button className = "RemoveButton" onClick = { ( ) = > props . dispatch ( { type : "REMOVE" , payload : props . topping } ) } > Remove < / button > < button className = "AddButton" onClick = { ( ) = > props . dispatch ( { type : "ADD" , payload : props . topping } ) } > Add < / button > < / React . Fragment > ) ; } ; export default listControl ;

You can find the complete working code here.

Get in touch with our developers today to understand the concept better.

Note : The reducer function should be defined separately outside the functional component. If it is defined within the component code, it implies that on a re-render, it provides a new reducer function each time even though the reducer code is identical. You may experience unexpected behavior if you define the reducer function code inside the component in terms of the hook getting called twice unexpectedly.

Check out this code and see if you notice something unexpected on clicking “Add” first.

If we still need to keep the reducer inside the function component due to dependencies on props or context or any other state, we can avoid the re-render by memoizing the reducer. We can make use of useCallback to memoize the reducer so that it only creates a new reducer when its dependencies change.

By making use of Context, we can have any level of deeply-nested components share the state and the reducer functionality by sharing a single method reference of the dispatch method.

Advantages of using the useReducer Hook:

It can be a single location to perform the initialization, as well as all, make changes to the state attribute by identifying and handling all the actions which need to be performed on the component state.

It will always have access to the previous state value to make any changes to it and return the latest state value. This is its underlying behavior and we don’t have to do anything additional to ensure that it always uses the latest state value.

It will always return the latest state. We do not have to use a setState call to set the state explicitly, because the state is automatically set to whatever value is returned by the reducer function.

If the same value is returned from the Reducer Hook as the current state, it is compared using the Object.is() logic. React will bail out without rendering the children or firing effects. In case of no real change, the re-render will not happen.

Instead of having multiple callback references passed down to the child components, the dispatch function of useReducer can be passed using context as a single touchpoint to all actions to be performed on state. Using the dispatch, any child component at any level can invoke an action at the parent level. This is extremely helpful for deeply nested complex components to have a single touchpoint and avoid multiple different callbacks.

How to define state using hooks?

What is the ideal way of defining the structure of the state argument?

While defining a React component, we will need to identify all the parts of the component state.

Once we have the parts identified, we can define State in any of the two ways mentioned below:

State can be combined as a single object.

This approach should be used if the state comprises of a related set of data, which will always change simultaneously.

For example, the user details, blog details etc.

const [users, dispatch] = useReducer(userReducer, initialState); 1 const [ users , dispatch ] = useReducer ( userReducer , initialState ) ;

The State elements can be split and defined as different pieces of state. This approach is used to segregate the different parts of the values that the state attribute defines which tend to change together. There can be multiple states defined for the component which are grouped based on whether they are updated together.

For example, the order ingredients list will change each time the user selection changes, but the Total order value will change only when the user confirms the order. So we can have 2 different states defined as below:

By segregating the different pieces of the state, each piece can have its own set of reducer actions handled separately.

const [ingredients, dispatchIngredients] = useReducer(ingredientsReducer, []); const [totalorder, dispatchTotal] = useReducer(totalReducer, null); 1 2 const [ ingredients , dispatchIngredients ] = useReducer ( ingredientsReducer , [ ] ) ; const [ totalorder , dispatchTotal ] = useReducer ( totalReducer , null ) ;

Conclusion

Grouping and defining the state properly and making use of hooks appropriately can boost the performance of the application by avoiding any unnecessary re-render. The useReducer hook is ideal for complex state objects that change more frequently. The state value is dependent on previous state value and also if it has deeply-nested child components that use and pass callbacks to the common state-change actions.

Call us today to connect with a REACT expert to know more about useReducer.

References: