Reducers Unleashed: Unlock State Management Mastery

Have you ever heard of the term ‘Reducers’ or ‘Reducer’ when working with React and Redux? If you’ve worked with React, you might have used state management libraries such as Redux, which employs reducers. In Redux, the reducer is one of the three main components, alongside the store and actions. However, in this post, I will focus exclusively on reducers.

What is a reducer?

Although ‘Reducer’ is a component of Redux, its use is not limited to Redux. You can utilize it in pure vanilla JavaScript projects, within React components without integrating Redux, with React Context, or alongside other state management libraries such as MobX, Recoil, or Zustand.

A reducer is nothing but a function. More specifically it is a pure function as it is called in functional programming. Now, let’s dive into the concept of a pure function

What are Pure functions ?

A pure function is a fundamental concept in computer science and functional programming. It is a type of function that has two key characteristics:

  1. Deterministic: A pure function always produces the same output for the same set of input arguments. This means that if you call a pure function with a specific set of inputs, it will consistently return the same result, regardless of when or where you call it.
  1. No Side Effects: A pure function does not have any side effects. Side effects refer to any observable changes or interactions that occur outside of the function’s scope while it’s executing. Side effects can include modifying global variables, changing state, performing I/O operations (like reading or writing to files or databases), or making network requests.

 Some examples of pure functions are:

  • Addition Function: Takes two numbers as input and returns their sum.
  • Multiplication Function: Takes two numbers as input and returns their product.
  • Sine Function: Takes an angle in radians as input and returns the sine of that angle.
  • Cosine Function: Takes an angle in radians as input and returns the cosine of that angle.
  • String Concatenation: Takes two strings as input and returns a new string which is the concatenation of the input strings.

Why Reducers are pure functions ?

Unlike functions like the Addition Function or Multiplication Function, a Reducer is not a specific predefined function, and it’s not necessarily a pure function on its own. Instead, a Reducer is a concept in which you implement a function, and it’s typically designed to be a pure function.

So, what makes Reducer a pure function? Because:

  1. It always returns the same output for the same input. This means that if you call a reducer with the same state and action twice, it will always return the same new state( Deterministic ).
  2. It has no side effects. This means that the reducer does not mutate any global state, make any API calls, or perform any other operations that could change the state of the application outside of the reducer itself.

Creating a Reducer

The simplest Reducer you can create 

A reducer is a pure function that takes at least one parameter. However, it is common to use two parameters: the current state and the action. The simplest reducer that you can create is the one that doesn’t actually modify the state but just returns the current state as is. 

This type of reducer is often used as a default or initial reducer when you want to handle actions that don’t require any state changes. 

function simpleReducer(state, action) {
  // No state changes, just return the current state
  return state;
}

In this reducer, regardless of the action type or payload, the state remains the same. While this may not be very useful on its own, it can be part of a larger reducer setup where you handle specific actions in other reducer functions and use the simple reducer as a fallback for unhandled actions.

Reducer with condition 

Reducers are often used in conjunction with a switch statement. The reason for this is that you can handle different types of actions, such as adding, deleting, updating, and more. The way you implement a reducer can vary depending on the specific requirements of the application. While the use of a switch statement is common in reducer functions, you could also employ other conditional logic or patterns if you prefer.

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

In many applications, you need to initialize the current state with an initial state. Thus, we can modify the above reducer as below.

const initialState = {
  count: 0,
};

function counterReducer(state = initialState , action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

Consuming the Reducer function

Now that you learn how to create a reducer, let’s discuss how you can use it. I will use the above counterReducer as an example.

// Example usage 
let currentState = initialState;
console.log(currentState); // { count: 0 } 

currentState = counterReducer(currentState, { type: 'INCREMENT' }); 
console.log(currentState); // { count: 1 } 

currentState = counterReducer(currentState, { type: 'DECREMENT' }); 
console.log(currentState); // { count: 0 }

How to use Reducers with arrays of objects

During application development, it’s common to retrieve data from external APIs. Typically, these APIs provide responses in the form of arrays of objects. For instance, let’s consider a scenario where a specific API sends the following array of objects, which we’ll treat as our initial state when launching the application.

const initialState = [
  {
    "id": 1,
    "name": "Product A",
    "price": 29.99
  },
  {
    "id": 2,
    "name": "Product B",
    "price": 39.99
  },
  {
    "id": 3,
    "name": "Product C",
    "price": 19.99
  }
]

Now , this would be our reducer.

export function productReducer( state , action ) { 
  switch( action.type ){
      case 'add_product': 
            return [...state, action.payload ]
      case 'remove_product':
            return state.filter( product => product.id !== action.payload.id );
      case 'update_product':
            return state.map(product =>  (
                 product.id === action.payload.id) ? 
         // If the product ID matches the updated product's ID, update it          
                   { ...product, ...action.payload }:           
        // Otherwise, leave the product unchanged
                   product        
      );
      default:
            return state
  } 
}

Take note of the ‘add product’ case. Utilize the spread operator (…) to create a duplicate of the current state, and then incorporate the new product (action.payload) into this fresh state. By doing so, you avoid directly altering the existing state. You can dispatch this action as demonstrated below.

let  productList ='';

//adding a new product 
productList = productReducer( initialState, {
  type: "add_product",
  payload:{ 
      "id": 4,
      "name": "Product D",
      "price": '40'
    }
})

When removing a product, you can utilize JavaScript’s filter method, and there’s no need for the spread operator in this case. Why? Because the filter method inherently returns a new copy of the array. In this example, our state is represented as an array of products, and when you apply the filter method, it generates a new array of objects that excludes the one you intend to remove from the list. This approach ensures that you maintain the immutability of the original state, which is crucial for proper state management.

//deleting a specific product
productList = productReducer( initialState, {
  type: "remove_product",
  payload:{ "id": 2 }
})

When updating a product in the array, we first check for the specific product to update. If no update is needed, we simply return the existing list of products (i.e., the current state). To ensure we don’t modify the current state directly, we employ the spread operator in the expression { …product, …action.payload }. This operation creates a new object by copying the properties from an existing object (product) and then overriding the properties with those from action.payload. It’s equivalent to

{ "id": 2, "name": "Product B","price": 39.99 ,  "id": 2, "name": "Product D", "price": 12.25  }

You can dispatch the action as :

//updating a specific product
productList = productReducer( initialState, {
  type: "update_product",
  payload: {
    "id": 2,
    "name": "Product D",
    "price": 12.25
  }
})

Conclusion


Reducers are a fundamental concept in state management within the context of JavaScript applications, particularly popularized by libraries like Redux. They are functions responsible for handling state updates in a predictable and controlled manner. Reducers take the current state and an action as input and return a new state as output. Crucially, reducers are designed to be pure functions, meaning they produce the same output for the same input and do not modify the existing state. This immutability ensures predictability and makes it easier to debug and reason about the state changes in an application. While commonly associated with libraries like Redux, reducers can also be employed in pure JavaScript projects, offering a structured and maintainable way to manage application state.

Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top