loading...
Cover image for What Is a Reducer in JavaScript? A Complete Introduction with Examples

What Is a Reducer in JavaScript? A Complete Introduction with Examples

codeartistryio profile image Reed Barger Originally published at reedbarger.com on ・7 min read

For most JavaScript applications, the reducer is an essential concept that helps us manage application state.

It is used in virtually every JavaScript library or framework, React, Angular and Vue, particularly in the state management libraries Redux and ngrx. It’s important to understand in order to grasp managing state in medium to large scale applications.

What is a reducer?

A reducer is a very simple idea and it’s something that will be easy for you to grasp because, in a nutshell, it’s just a simple JS function.

A reducer is a function which takes two arguments — the current state and an action — and returns based on both arguments a new state.

We can express the idea in a single line, as an almost valid function:

const reducer = (state, action) => newState;
Enter fullscreen mode Exit fullscreen mode

Let’s take a very simple example where we need to manage some data, say our app has acounter, where we can increment or decrement a number by 1. So let’s take our reducer and call it counterReducer. This function will be executed to update state whenever a user wants to count up or down. As a result in the function body, we just want to return state + 1:

function counterReducer(state, action) {
  return state + 1;
}
Enter fullscreen mode Exit fullscreen mode

So for now our counter increments just by 1 every time.

If this looks confusing we can rename state to count:

function counterReducer(count, action) {
  return count + 1;
}
Enter fullscreen mode Exit fullscreen mode

Let’s say the initial state is 0, after running this, we expect the result to be 1. And it is:

counterReducer(0) === 1; // true
Enter fullscreen mode Exit fullscreen mode

What is so special about this and why would we want to use it?

First of all, reducers are special because they are predictable. In other words, they are the real-world example of the pure functions which, given a certain input, we will always have the same output with no side effects (an interaction with something outside our app that can change our state, such as an API) along the way. This is ideal for doing something that we need to have reliable values for like managing state.

Actions

We haven’t touched on the reducer’s second argument however, the action. This actions allows us to communicate to the reducer that we want to perform a different state update. For example, we may not always want to increase the counter. We may want to decrement the count and therefore the state. We communicate that through the action.

What is the action? It’s just a simple JavaScript object that first says the type of action the user wanted to perform.

If a user want to increase the count, the action looks like this:

{ type: INCREMENT }; // action to increment counter
Enter fullscreen mode Exit fullscreen mode

We provided the type of the action we want or the user wants to perform on the type property. The type is a string and the convention is to make it uppercase, like a constant, to make it as clear as possible.

Now what about for the decrement action. Stop for a minute and see if you can do that on your own:

{ type: DECREMENT } // action to decrement counter
Enter fullscreen mode Exit fullscreen mode

Now we have to add some additional logic within our reducer to update state appropriately according to the type.

You might think that using an if / else would be appropriate, but note that some reducer can have many, many conditions, which makes the switch statement a better and more concise choice.

So let’s rewrite our function:

function counterReducer(count, action) {
  switch (action.type) {
    case "INCREMENT":
      return count + 1;
    case "DECREMENT":
      return count - 1;
    default:
      return count;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can use a return here instead of break because we can exit the function entire after the conditional is run. We are not going to have anything else in our reducers other than this conditional.

And also, if we mistakenly pass in an action to this reducer that doesn’t match any one of the cases, it will just run the default case and return the current state.

So let’s test this out again. Let’s increment and then decrement our counter:

counterReducer(0, { type: INCREMENT }) // 1
Enter fullscreen mode Exit fullscreen mode

So first we have 1, then let’s take that 1 and decrement it and we should have 0:

counterReducer(1, { type: DECREMENT }) // 0
Enter fullscreen mode Exit fullscreen mode

And we do.

The immutability of reducers

In our applications we will have more complex values than just a single number. It will likely never be a JS primitive in state, but an object that we use to organize our information. Which makes sense. On an object we can both organize and manage a lot more data in an orderly way.

So let’s reimagine our example with an object and instead of having count be the entire state, we’ll have an entire state object with multiple properties. We also know that pure functions need to be immutable, so how do we do that now for a state value that’s an object?

First let’s change count to state. And count is now just a property on state:

function counterReducer(state, action) {
  switch (action.type) {
    case "INCREASE":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now here we are just updating the count property, but say if we had other properties, we would want to merge them into a single object with the count state as well. We could easily do that with the spread operator like so:

function counterReducer(state, action) {
  switch (action.type) {
    case "INCREASE":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

This is an essential part of using reducers in managing application state. State is managed largely through objects and state updates must always be immutable. We create a new state object from the incoming state and the part we want to change (e.g. count property). This way we ensure that the other properties that aren’t touch from the incoming state object are still kept intact for the new state object. So this pattern of spreading in the old state and updating a single piece of state that the reducer controls to create a new object will become a very familiar pattern

Let’s make a new reducer that controls the current user’s name and email. Because it’s going to manage the user’s state we will call this the user reducer and have state and action as parameters. We’ll make two cases, one to change their name and another to change the email.

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
    case "CHANGE_EMAIL":
  }
}
Enter fullscreen mode Exit fullscreen mode

Payloads

At this point, we’ve touched on how to use types to run one or another condition, but here we need to pass more information to our reducer to update state appropriate. Let’s say the user updates their names through two inputs. How do we receive the values they’ve typed in?

We still receive it through the action, but through another property called the payload. On this property, we can accept whatever data we like. Let’s write it for the CHANGE_NAME condition when a user changes their name.

We could set the payload to whatever the users’ typed in, but a better way to handle this is to make payload an object. This is so we can pass multiple values on the payload and each of these values will be very clear as to what they are. For example, we can give the object the property name when running change name

{ type: 'CHANGE_NAME', payload: { name: 'Joe' } }
Enter fullscreen mode Exit fullscreen mode

Then back within our switch, to update state, we can return and object where we spread in all the other state properties that we aren’t updating to the new state object. And then to get the payload to update the name, let’s say that the initialState consists of a name and email property:

const initialState = {
  name: "Mark",
  email: "mark@gmail.com",
};
Enter fullscreen mode Exit fullscreen mode

We can just set the name property to action.payload.name. It’s that simple. Since it’s a primitive value, not a reference value we don’t need to worry about copying here:

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
      return { ...state, name: action.payload.name };
    case "CHANGE_EMAIL":
  }
}
Enter fullscreen mode Exit fullscreen mode

And we can do the same for the email. Let’s write the action first:

{ type: 'CHANGE_EMAIL', payload: { email: 'mark@compuserve.com' } }
Enter fullscreen mode Exit fullscreen mode

And then the condition, make sure to provide our default case at the end. And note that it doesn’t have the keyword case in front of it, just default:

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
      return { ...state, name: action.payload.name };
    case "CHANGE_EMAIL":
      return { ...state, email: action.payload.email };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let’s perform these state updates, passing in the initialState:

const initialState = {
  name: "Mark",
  email: "mark@gmail.com",
};

function userReducer(state, action) {
  switch (action.type) {
    case "CHANGE_NAME":
      return { ...state, name: action.payload.name };
    case "CHANGE_EMAIL":
      return { ...state, email: action.payload.email };
    default:
      return state;
  }
}

const action = {
  type: "CHANGE_EMAIL",
  payload: { email: "mark@compuserve.com" },
};

userReducer(initialState, action); // {name: "Mark", email: "mark@compuserve.com"}
Enter fullscreen mode Exit fullscreen mode

Summary

You’ll become more confident with reducers as you use them in your own applications. They should be a concept that simplifies our code by helping us make our state updates more predictable.

Here are the essential things you should know about a reducer going forward:

  • Syntax: In essence a reducer function is expressed as (state, action) => newState.
  • Immutability: State is never changed directly. Instead the reducer always creates a new state.
  • State Transitions: A reducer can have conditional state transitions.
  • Action: A common action object comes with a mandatory type property and an optional payload:The type property chooses the conditional state transition.The action payload provides information for the state transition.

Discussion

pic
Editor guide
Collapse
codingsafari profile image
Nico Braun

Why is this better than haven an object or class with the appropriate named functions? Why having a single function and then using a switch statement? At least this example implementation seems imperformant and error prone to me.