Getting to Know the useReducer React Hook

useReducer is one of a handful of React hooks that shipped in React 16.7.0. It accepts a reducer function with the application initial state, returns the current application state, then dispatches a function.

Here is an example of how it is used;

const [state, dispatch] = useReducer(reducer, initialState);

What’s the good for? Well, think about any situation where having the first loaded state of the application might be nice. Let’s say the starting point on an interactive map. Maybe it’s an app that lets the user build a custom car with custom options from a default model. Here’s a pretty neat demo of a calculator app that puts useRedcuer to use in order to reset the calculator to a default state of zero when clearing it out.

See the Pen
Basic React Hook Calculator
by Gianpierangelo De Palma (@dpgian)
on CodePen.

We’re going to dig into a couple more examples in this post, but let’s first look at the hook itself to get a better idea of what it is and what exactly it does when it’s used.

The almighty reducer

It’s tough to talk about useState without also mentioning JavaScript’s reduce method. We linked it up at the very top, but Sarah’s post is an excellent overview of reducers and helps set the state for where we’re going here.

The first and most important thing to understand about a reducer is that it will always only return one value. The job of a reducer is to reduce. That one value can be a number, a string, an array or an object, but it will always only be one. Reducers are really great for a lot of things, but they’re especially useful for applying a bit of logic to a group of values and ending up with another single result.

So, if we have an array of numbers, reduce will distill it down to a single number that adds up for as many times as there are values. Say we have this simple array:

const numbers = [1, 2, 3]

…and we have a function that logs each time our reducer makes a calculation into the console. This will help us see how reduce distills the array into a single number.

const reducer = function (tally, number) {  	console.log(`Tally: $  {tally}, Next number: $  {number}, New Total: $  {tally + number}`) 	return tally + number }

Now let’s run a reducer on it. As we saw earlier, reduce takes dispatches a function that runs against a default state. Let’s plug our reducer function and an initial value of zero in there.

const total = numbers.reduce(reducer, 0)

Here’s what gets logged to the console:

"Tally: 0, Next number: 1, New Total: 1" "Tally: 1, Next number: 2, New Total: 3" "Tally: 3, Next number: 3, New Total: 6"

See how reduce takes an initial value and builds on it as each number in the array is added to it until we get a final value? In this case, that final value is 6.

I also really like this (modified) example from Dave Ceddia that shows how reduce can be used on an array of letters to spell a word:

var letters = ['r', 'e', 'd', 'u', 'c', 'e'];  // `reduce` takes 2 arguments: //   - a function to do the reducing (you might say, a "reducer") //   - an initial value for accumulatedResult var word = letters.reduce( 	function(accumulatedResult, arrayItem) { 		return accumulatedResult + arrayItem; 	}, ''); // <-- notice this empty string argument: it's the initial value  console.log(word) // => "reduce"

useReducer works with states and actions

OK, that was a lot of refresher to get what we’re really talking about: userReducer. It’s important to get all this, though, because you may have noticed where we’re going now after having seen the way reduce fires a function against an initial value. It’s the same sort of concept, but returns two elements as an array, the current state and a dispatch function.

In other words:

const [state, dispatch] = useReducer(reducer, initialArg, init);

What’s up with that third init argument? It’s an optional value that will lazily create the initial state. That means we can calculate the initial state/value with an init function outside of the reducer instead of providing an explicit value. That’s handy if the initial value could be different, say based on a last saved state instead of a consistent value.

To get it working, we need to do a few things:

  • Define an initial state.
  • Provide a function that contains actions that update the state.
  • Trigger userReducer to dispatch an updated state that’s calculated relative to the initial state.

The classic example of this a counter application. In fact, that’s what React’s docs use to drive the concept home. Here’s that put into practice:

See the Pen
React useReducer 1
by Kingsley Silas Chijioke (@kinsomicrote)
on CodePen.

It’s a good example because it demonstrates how an initial state (a zero value) is used to calculate a new value each time an action is fired by clicking either the increase or decrease button. We could even throw in a “Reset” button in there to clear the total back to the initial state of zero.

Example: A Car Customizer

See the Pen
React useReducer – car example
by Geoff Graham (@geoffgraham)
on CodePen.

In this example, we are making the assumption that the user has selected a car to purchase. However, we want the app to allow the user to add extra options to the car. Each option has a price that adds to the base total.

First, we need to create the initial state which will consist of the car, an empty array to keep track of features, and an additional price that starts at $ 26,395 and a list of items in the store, so the user can pick what they want.

const initialState = {   additionalPrice: 0,   car: {     price: 26395,     name: "2019 Ford Mustang",     image: "https://cdn.motor1.com/images/mgl/0AN2V/s1/2019-ford-mustang-bullitt.jpg",     features: []   },   store: [     { id: 1, name: "V-6 engine", price: 1500 },     { id: 2, name: "Racing detail package", price: 1500 },     { id: 3, name: "Premium sound system", price: 500 },     { id: 4, name: "Rear spoiler", price: 250 }   ] };

Our reducer function will handle two things: the addition and removal of new items.

const reducer = (state, action) => {   switch (action.type) {     case "REMOVE_ITEM":       return {         ...state,         additionalPrice: state.additionalPrice - action.item.price,         car: { ...state.car, features: state.car.features.filter((x) => x.id !== action.item.id)},         store: [...state.store, action.item]       };     case "BUY_ITEM":       return {         ...state,         additionalPrice: state.additionalPrice + action.item.price,         car: { ...state.car, features: [...state.car.features, action.item] },         store: state.store.filter((x) => x.id !== action.item.id)       }     default:       return state;   } }

When the user selects the item she wants, we update the features for the car, increase the additionalPrice and also remove the item from the store. We ensure that the other parts of the state remain as they are.
We do something similar when a user removes an item from the features list – reduce the additional price, return the item to the store.
Here is how the App component looks like.

const App = () => {   const inputRef = useRef();   const [state, dispatch] = useReducer(reducer, initialState);      const removeFeature = (item) => {     dispatch({ type: 'REMOVE_ITEM', item });   }      const buyItem = (item) => {     dispatch({ type: 'BUY_ITEM', item })   }      return (     <div>       <div className="box">         <figure className="image is-128x128">           <img src={state.car.image} />         </figure>         <h2>{state.car.name}</h2>         <p>Amount: $  {state.car.price}</p>         <div className="content">           <h6>Extra items you bought:</h6>           {state.car.features.length ?              (               <ol type="1">                 {state.car.features.map((item) => (                   <li key={item.id}>                     <button                       onClick={() => removeFeature(item)}                       className="button">X                     </button>                     {item.name}                   </li>                 ))}               </ol>             ) : <p>You can purchase items from the store.</p>           }         </div>       </div>       <div className="box">         <div className="content">           <h4>Store:</h4>           {state.store.length ?              (             <ol type="1">               {state.store.map((item) => (                 <li key={item.id}>\                   <button                     onClick={() => buyItem(item)}                     className="button">Buy                   </button>                   {item.name}                 </li>               ))}             </ol>             ) : <p>No features</p>           }         </div>          <div className="content">         <h4>           Total Amount: $  {state.car.price + state.additionalPrice}         </h4>       </div>       </div>     </div>   ); }

The actions that get dispatched contains the details of the selected item. We make use of the action type to determine how the reducer function will handle the updating of the state. You can see that the rendered view changes based on what you do – buying an item from the store removes the item from the store and adds it to the list of features. Also, the total amount gets updated. No doubt, there are some improvements that can be done to the application, this is only for learning purpose.

What about useState? Can’t we use that instead?

An astute reader may have been asking this all along. I mean, setState is generally the same thing, right? Return a stateful value and a function to re-render a component with that new value.

const [state, setState] = useState(initialState);

We could have even used the useState() hook in the counter example provided by the React docs. However, useReducer is preferred in cases where state has to go through complicated transitions. Kent C. Dodds wrote up a explanation of the differences between the two and (while he often reaches for setState) he provides a good use case for using userReducer instead:

If your one element of your state relies on the value of another element of your state, then it’s almost always best to use useReducer

For example, imagine you have a tic-tac-toe game you’re writing. You have one element of state called squares which is just an array of all the squares and their value[.]

My rule of thumb is to reach for useReducer to handle complex states, particularly where the initial state is based on the state of other elements.

Oh wait, we already have Redux for this!

Those of you who have worked with Redux already know everything we’ve covered here and that’s because it was designed to use the Context API to pass stored states between components — without having to pass props through other components to get there.

So, does useReducer replace Redux? Nope. I mean, you can basically make your own Redux by using it with the useContext hook, but that’s doesn’t mean Redux is useless; Redux still has plenty of other features and benefits worth considering.

Where have you used userReducer? Have you found clear-cut cases where it’s better than setState? Maybe you can experiment with the things we covered here to build something. Here are a few ideas:

  • A calendar that focus at today’s date but allows a user to select other dates. Maybe even add a “Today” button that returns the user to today’s date.
  • You can try improving on the car example – have a list of cars that users can purchase. You might have to define this in the initial state, then the user can add extra features they want with a charge. These features can be predefined, or defined by the user.

The post Getting to Know the useReducer React Hook appeared first on CSS-Tricks.

CSS-Tricks

Comments

comments

, , , ,