DEV Community

Cover image for Redux for beginners
CaptainPrinz
CaptainPrinz

Posted on

Redux for beginners

Introduction

Redux is still regarded as being a notoriously difficult library to work with by entry-level developers despite dominating the frontend world. This article aims to explain the basics of Redux.

Why developers avoid Redux

The following factors might be reasons why developers tend to avoid Redux:

  • Huge Boilerplate code
  • Steep Learning Curve

Even with the reasons mentioned above Redux is still one of the most used state management libraries among enterprises, startups and Independent developers. One such enterprise in Robinhood.

Redux Architecture

While Redux's boilerplate is one of the primary reasons why it is avoided by developers, it doe have a simple architecture; thereby simplifying the whole process of managing state in applications.

The Redux Architecture is made up of three basic components:

  1. Store

  2. Reducers

  3. Action

Store

The store is essentially a container that stores the application's global state. There can only be a single store in a Redux application. Data kept in a Redux store cannot be changed. The only way to change the data already stored in a Redux store is to dispatch an action.

Under the hood, a Redux store is just a plain object containing state data that can be accessed at all levels within the application. However, the store has some special methods to control the flow of state data.

The most important methods include:

  1. getState()

  2. dispatch()

  3. subscribe()

The getState() method returns the state data tree persisted in the store.

The dispatch() method is used to dispatch an action to the store to change the data stored in the application. It takes an action as an argument.

The subscribe() method allows us to run a callback function an action is dispatched. This provides a way to get the recent data in the store by calling the getState() method.

The createStore() function allows us to create a store by taking a reducer function as an argument.

import { createStore } from "redux"

const store = createStore(reducer)
Enter fullscreen mode Exit fullscreen mode

Reducer

The Reducer is a function that contains the logic of how state data is saved in the store. The Reducer function simply takes the current state and the dispatched action as arguments and returns a new state based on the arguments.

The code below shows a basic implementation of a reducer function.

const initialState = {value: 0}

const myReducer = (state=initialState, action) => {
  if(action.type === "INCREMENT"){
    return {
      ...state,
      value: state.value + 1
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the initial state variable represents the current state in the application and a new state was returned based on the action that was dispatched.

However, a Redux application usually has more than one Reducer, each storing unique data for use in different parts of the application. Luckily redux provides us with the combineReducer() function.

The combineReducer() function takes an object whose values are reducing functions and returns a single reducer. This helps in projects with a large codebase where modularisation is needed for easy code maintenance.

const combination = combineReducers({
  posts: postReducer,
  counts: countReducer
})
Enter fullscreen mode Exit fullscreen mode

The state in each reducer can be easily accessed by using the keys provided in the combineReducers function.

Since Reducers are pure functions, the following rules must be observed:

  • Reducer functions should not contain side effects like asynchronous functions.

  • Reducers should not modify the current state directly.

  • To modify the current state, reducers should make a copy of the current state and return the modification as a new state.

But One may ask, If Reducers can't run asynchronous functions, then how does one deal with data fetching in a Redux application? This is where middlewares come in.

Middlewares

A middleware is a function executed between the period an action is dispatched and before it gets to the reducer. This middleware, in turn, provides an environment to run asynchronous functions and other side effects, particularly data fetching. The resulting data from the side effects function is then passed to the Reducer to complete the data manipulation cycle.

In short, Reducers determine how the state data is changed and middleware intercept actions before they are passed to the Reducers.

Action

Actions are simply events which trigger a change in State. They sort of define a particular task in an application. For example, suppose a counter application has buttons for incrementing and decrementing a count variable, the task of incrementing and decrementing the count variable could be considered an action in redux.

In Redux, an action is a plain javascript object with two properties: type and payload.

  • The type property describes the action to be dispatched.

  • The payload property allows us to attach some data to the action object so we can access it in the Reducer to save it in the store.

Below is a simple implementation of Redux actions.

const action = {
  type: "INCREMENT"
}

const anotherAction = {
  type: "SAVE_TODO",
  payload: "Get Groceries"
}
Enter fullscreen mode Exit fullscreen mode

If we consider the second action in the code above we could write a reducer function to access the payload and save it in the store. The implementation can be found below.

const initialState = {todos: []}

const anotherReducer = (state=initialState, action) => {
  if (action.type === "SAVE_TODO"){
    return {
      ...state,
      todos: [...state.todos, action.payload]
    } 
  }
}
Enter fullscreen mode Exit fullscreen mode

This aspect wouldn't be complete without mentioning action creators.

Action creators are functions that return actions. Action creators allows us to create actions on the fly.

const actionCreator = (type, payload) => {
  return {
    type,
    payload
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now create actions easily by passing the necessary parameters to the function.

const customAction = actionCreator("DELETE_TODOS", "Buy Groceries")
// returns {type: "DELETE_TODOS", payload: "Buy Groceries"}
Enter fullscreen mode Exit fullscreen mode

With the above implementation, we can pass payloads dynamically.

Stitching it together

At this moment, you should have grasped the basic concepts of Redux. We go further by building a simple counter application in Redux.

The application will have the following:

  • An HTML template

  • Two actions for incrementing and decrementing

  • A reducer function for the actions

  • A store for persisting data

We start by creating the UI template. We could decide to use the default value from the store but in this case, I'm just going to write the default value in the HTML.

<h1 id="target">0</h1>

<button id="increment">Increment</button>
<button id="decrement">Decrement</button>
Enter fullscreen mode Exit fullscreen mode

We then create the Reducer function.

const countReducer = (state={value: 0}, action) => {
  if(action.type === "INCREMENT"){
    return {
      ...state,
      value: state.value+1
    }
  } else if(action.type === "DECREMENT"){
    return {
      ...state,
      value: state.value-1
    }
  } else {
    return state
  }
}
Enter fullscreen mode Exit fullscreen mode

As a rule of thumb, you should always return the current state if the action doesn't match in your reducer function.

We then move on to create the store and use the subscribe() method to update the DOM when actions are dispatched.

const store = createStore(countReducer)

store.subscribe(() => {
 const { count } = store.getState()
 document.querySelector("#target").innerText = count
})
Enter fullscreen mode Exit fullscreen mode

Finally, we create the actions and dispatch them when the buttons are clicked.

const increaseCount = {type: " INCREMENT"}
const decreaseCount = {type: "DECREMENT"}

document.querySelector("#increment").onclick = () => {
  store.dispatch(increaseCount)
}

document.querySelector("#decrement").onclick = () => {
  store.dispatch(decreaseCount)
}
Enter fullscreen mode Exit fullscreen mode

The snippet below shows the complete implementation.

Conclusion

Save for its huge boilerplate, Redux can greatly simplify code maintainability, especially for projects with large codebases.

Other concepts will be discussed in subsequent articles.

If you find this article helpful, please leave a comment and suggestions on other web technologies.

Happy Learning!

Top comments (0)