A Beginner's guide to Redux

What is Redux ? Store, Actions and Reducers

ยท

7 min read

A Beginner's guide to Redux

Redux is a state management library that you can use with any JavaScript library or framework like React, Vue, Angular.

Our Target Problem:

In most cases when we create small applications, we use useState hook to manage the state of our components. In some cases, we would be required to pass the state of a component to another, which would be done by passing the state of the parent component as props to the child component ( known as prop drilling ).

As the application grows it becomes tedious to manage the state of the components of our application through prop drilling as we might be required to pass the state through various levels in the architecture.

Consider the following image

How to Build a Redux-Powered React App

This is the common component hierarchy in a React Application.

Let's say we need to share a common state between Child 5 and Child 6. In that case we can very well declare a state in their parent (that is, Child 2) and pass the state down to the two children (5 and 6).

All good as of now. But what if we need to have the same piece of state in Child 3? In that case we would need to declare the state in the common parent/grandparent of Children 5, 6, and 3 โ€“ that is, the App component.

Similarly what if we want to share a state between Children 4, 11, and 10 which are far away from each other in the tree? We would again need to create the state in the App component and then do multiple levels of prop drilling to pass the state from App to these components.

And as time passes and our app grows in size, it will start making our App component or any other such common parent component cluttered with unnecessary state declarations. These declarations aren't used directly by these components but are used by some of their far-down children.

Here are some disadvantages of prop drilling:

  1. Complexity and Maintenance Issues: As you pass down props through multiple levels, the application can become harder to follow, understand, and maintain. It's not immediately clear where a prop is coming from or how many levels it has passed through.

  2. Unnecessary Renders: Passing props through components that don't use them can lead to unnecessary renders if the parent component's state changes. Even if the intermediate component doesn't use a prop, it may re-render because its props object has changed.

  3. Tight Coupling: Prop drilling often results in components that are tightly coupled with one another. A change in a top-level component might necessitate changes in deeply nested children, leading to fragile code that's hard to refactor.

  4. Verbose Code: As you have to pass down props through multiple layers manually, it often results in more verbose code.

  5. Hard to Track Data Flow: Especially in larger applications, prop drilling can make it hard to track the data flow. It's not always clear which component is responsible for a certain piece of state or how it's being passed down.

  6. Refactoring Challenges: If a new component in the hierarchy requires access to the data, or if a component no longer needs the data, refactoring can become cumbersome. You'd need to adjust the props at each level of the component hierarchy.

  7. Error-Prone: The more props you pass around manually, the easier it is to make a mistake, such as missing a prop or sending the wrong one.

  8. Scalability Issues: As the application grows and evolves, managing the flow of props becomes increasingly challenging. This can lead to situations where lifting state up becomes necessary, resulting in even more prop drilling.

The Solution:

Due to these challenges, React developers often turn to state management solutions like Redux, Context API, MobX, and others to manage shared state more effectively and avoid excessive prop drilling.

Redux centralizes the application state in a single store, making it accessible to any component. By dispatching actions and using reducers to modify the state, it ensures predictable state updates. Components can connect directly to the Redux store, eliminating the need for prop drilling. This streamlines data flow and makes state management more scalable and maintainable.

This article covers the basics of Redux along with three basic principles of Redux

As a prerequisite, I would be assuming you're all familiar with React js.

  1. What is Redux Store?

The Redux store is a centralized container for the entire state of a Redux application, facilitating predictable state management through actions and reducers.

The Redux store is an object that holds the entire state tree of your application. It's the single source of truth for the state in a Redux app.

The state of the application can be accessed from the Redux Store by providing the store to the App.js i.e., by wrapping the <app /> with the <provider></provider> as shown in the code snippet. This makes it a global state similar to global variables in PHP.

import React from 'react';

// redux stuff
import { createStore } from 'redux';
import reducer from './reducer';
import { Provider } from 'react-redux';

const initialStore = {};

const store = createStore(reducer, initialStore);

function App() {
  //  Setup
  return (
    <Provider store={store}>
    </Provider>
  );
}

export default App;
  1. What are actions in Redux?

Actions in Redux are payloads of information that send data from your application to your store. They are the only way to get data into the store. Here's a more detailed breakdown:

Definition: An action is a plain JavaScript object that has a type property, which is typically a string constant. This type allows for understanding what kind of state change is being requested.

Structure: While the type property is essential, you can have other properties in the action to provide additional information about the action being performed.

const BookComponent = () => {

    // `useDispatch` is a hook provided by `react-redux` to dispatch actions.
    const dispatch = useDispatch();

    const addItemToCart = () => {
        return {
            type: "ADD_ITEM_TO_CART",
            payload: {
               productId: '123',
               name: 'Laptop',
               price: 1000,
               quantity: 1
            }
        };
    }
//Rest of code

In the above example, the BookComponent is a React functional component that represents a product, specifically a laptop. Using the useDispatch hook from react-redux, it can dispatch actions to the Redux store. Within this component, there's a function addItemToCart that, when called, returns an action with a type of "ADD_ITEM_TO_CART" and a payload detailing the product. This action can then be dispatched to the Redux store to update the cart state, although the dispatching part isn't shown in the provided snippet. The "Rest of code" comment indicates there's more to the component, likely the UI elements and other functionalities.

  1. What are reducers in Redux?

Definition: A reducer is a pure function that takes the current state and an action as arguments and returns a new state.

Pure Function: This means that for the same input (state and action), a reducer will always produce the same output (new state) without causing any side effects.

Structure: The typical structure of a reducer is to use a switch statement to handle different action types:

function cartReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_ITEM_TO_CART':
      // Logic to add item to cart
      return newState;
    case 'REMOVE_ITEM_FROM_CART':
      // Logic to remove item from cart
      return newState;
    // ... other case statements ...
    default:
      return state;
  }
}

Initial State: Reducers can have an initial state, which is the default state before any actions are dispatched. In the example above, initialState represents the initial state.

Immutability: One of the critical principles in Redux is that state is immutable. This means that when you change the state in a reducer, you should return a new state object instead of modifying the original state. Tools like the spread operator (...state) in JavaScript or libraries like Immer can be handy for this.

Combining Reducers: For larger applications, managing all state within a single reducer can become unwieldy. Redux provides a utility called combineReducers that lets you split your reducer logic across multiple smaller reducer functions, each managing its own part of the state.

import { combineReducers } from 'redux';
import cartReducer from './cartReducer';
import userReducer from './userReducer';

const rootReducer = combineReducers({
  cart: cartReducer,
  user: userReducer
});

Handling Actions: In response to an action, the reducer produces the next state. For instance, when an 'ADD_ITEM_TO_CART' action is dispatched, the reducer might add the item (found in action.payload) to the list of items in the cart and return a new state with that item added.

Summary:

In short, the following three principles completely govern how Redux works:

  1. The entire application's state resides in a single store represented as an object tree.

  2. Actions, represented as descriptive objects, are the sole mechanism to introduce changes to the state.

  3. Pure reducers dictate how the state evolves in response to these actions.

Wrapping up:

Thank you for diving in! I sincerely hope you found this Redux overview both enlightening and valuable.

Please think about sharing it with your peers; your support means a lot. Keep an eye out for more engaging content ahead. Take care and see you later! ๐Ÿ––

ย