React-Redux, A Common Sense Approach

React-Redux, A Common Sense Approach

Imagine placing an order to your favorite food vendor/restaurant by simply calling the restaurant's call center.

The process is as simple as picking up your phone, and dialing the restaurant's number, "ring ring!!" and from the other side someone picks up to say:

  • Attendant - Hello? How may we help you today?
  • You - Can I have pizza and a bottle of coke
  • Attendant - (Writes down your order), anything else?
  • You - No, How long will it take?
  • Attendant - 15 minutes, thank you for choosing us.

The attendant goes ahead to take your written order to the chef, who looks at it to determine what type of pizza you require, the ingredients, and any other requirements/preferences of yours from the list.

5 minutes later the attendant gets the prepared pizza from the chef, calls the available delivery agent who delivers the pizza to your address.

10 minutes later the rider is at your doorstep with the hot, chicken pizza (Why am I hungry all of a sudden).

Great! Now, let's extract the key players from the above story:

  1. You
  2. The restaurant
  3. Call attendant
  4. Delivery agent
  5. Chef
  6. The list the attendant wrote down your order
  7. Your delivery package

How are all these related to react-redux?

The major aim of react-redux as a state management tool is to have a centralized data management technique and structure (A store) that allow React components access and makes changes to required data irrespective of their status (parent or child) on the component tree using hooks like [useSelector] and [useDispatch].

This is made possible by Actions (Synchronous and Asynchronous) and Reducers. Actions in react-redux are simply functions that return an object which specifies what exactly the component wants to do to or with the data managed in the centralized position (Store).

Ok! I know that's a lot of grammar, but let's look at the key terms in comparison to the restaurant's analogy.

  1. Store - Restaurant
  2. Required Data - Pizza
  3. React Component - You
  4. UseDispatch - Call attendant that receives your order
  5. Reducer - Chef
  6. Payload - List written down by the call attendant and handed over to the chef
  7. Actions - The process of handing the list over to the chef, or the act of telling the chef which type of pizza to make
  8. UseSelector - A delivery agent that brings your pizza to you

ACTIONS

const prepareChickenPizza = (payload) => ({
    type: 'PREPARE_CHICKEN_PIZZA',
    payload
});

Above is an example of synchronous action. This is because it does not have any side effects, does not make any call to an external API, and is referred to it as a pure function.

We made use of the ES6 arrow function to depict the action which accepts a payload and returns an object of key-value pairs:

  • Type: Short description of what the reducer should do, also known as an Address
  • Payload: This can be any data type that the reducer uses to fetch or make changes to the state of data in the store.

REDUCERS

As described in our story, the reducer is the chef or the manager of the store and has the sole privilege of retrieving and making changes to data in the store.

It is a function that receives data (data in the store can be retrieved or changed ) and an object with key-value pairs of type and payload as parameters. Reducers use the switch expression to evaluate the type (address) from the object returned by the action passed as a parameter.

Linking the data the reducer receives to our analogy, it is all the ingredients available to the chef which is used to make your pizza based on the type and user's pizza preferences (payload) specified in the list given by the call attendant.

// PS: I will be using pseudo code in some parts to explain what is done

const pizzaManager = (state = storeData, action) => { 
    const { type, payload } = action; // using object destructuring to get variables of type and payload from the action object.
    switch (type) {
         CASE PREPARE_CHICKEN_PIZZA:
               return `modify [state] to produce pizza based on the payload`
         default:
               return `meaning no preferences defined, give the user the default pizza`
    }

}

Remember how computer algorithm is similar to our everyday life? The reducer above depicts what goes on in the brain of the chef and how it arrives at what pizza to make from your preferences or not.

Few things to note about reducers:

  • The state received as a parameter is immutable (This means that you cannot change the whole data, rather you can create a copy and make changes to a particular section)
  • You can have as many case statements as you have addresses or types

Why is react-redux important

The basis of many applications is managing and collecting data, and react makes use of component-based web development techniques, where you can have several sub-component coming together to make a single module of functionality. For example, You may need to create a page with a section that displays a list of cards.

That section can be abstracted as a component that receives the data to be presented in the list of cards, another subcomponent that handles your list and styling for the wholistic cards when rendered, and finally, a component that handles your list item (the individual cards) including styling and all of its peculiarities.

This makes the development of applications even more scalable and manageable.

In React, we are all aware of the concept of state. Traditionally, for the data to be accessed in the sub-components, the parent component in this case the section where the data was received will have to pass down the data to the list component through props who in turn, pass required data to the item component for rendering.

Similarly, if there is any data needed by the parent component which is located in the child component, the parent component would have to require that through a concept called "Lifting state up".

If you had a few components, this process described above would be ideal. But when your application grows and you have so many components and subcomponents, you will notice that data might also be shared among components without a common ancestor, and this will require you to do a lot of manipulations to get these data to the required component, this makes your project less scalable and unnecessarily ambiguous.

With react-redux, you set up a store that handles the data and all components can access it irrespective of their position on the component tree. This does not stop you from passing around data through props when needed.

Bringing it all together

We have done a lot of descriptions and analogies, now let's write a program that required pizza from a store. Here, I will show you how functional components make requests, how the store is made available to all components in the project, and practical usage of the reducer and actions.

You will need to install react-redux:

npm i -S react-redux

Setting up actions, state, reducers, and store

manager.js

// define addresses, this is done so that the address 
// will not be changed mistakenly in the reducer

const PREPARE_PIZZA = 'PREPARE_PIZZA';
const PREPARE_SOUP = 'PREPARE_SOUP';
const DELIVER_DRINK = 'DELIVER_DRINK';

// define your store's initial data;

const storeData = {
    'pizza': 'pepperoni',
    'drinks': 'wine',
    'soup': 'curry',
}

// define actions

export const preparePizza = (payload) => ({
    type: PREPARE_PIZZA,
    payload,
});

export const prepareSoup = (payload) => ({
    type: PREPARE_SOUP,
    payload,
});

export const deliverDrinks = (payload) => ({
    type: DELIVER_DRINK,
    payload,
});

// define your reducer

const resturantChef = (state = storeData, action) => {
    const { type, payload } = action;

    switch (type) {
        case PREPARE_PIZZA
             return { ...storeData, junks: payload };
        case PREPARE_SOUP
             return {...storeData, soup: payload};
        case DELIVER_DRINK
             return {...storeData, drinks: payload};
        default
             return storeData;
    }
}

export default resturantChef;

Next, we create our store and assign the reducer to it as a manager.

resturant.js

import { createStore } from 'redux';
import resturantChef from './manager';

// creating a store
const store = createStore(resturantChef);

export default store;

Finally, we make this store accessible to all components by wrapping all our components in the index.js file with a special component called the provider.

import { Provider } from 'react-redux';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import store from 'resturant';
import DisplayFood from 'components/DisplayFood';
import userProfile from 'components/UserProfile';

ReactDOM.render(
  <Provider store={store}>
    <React.StrictMode>
      <Router basename={process.env.PUBLIC_URL}>
          <Routes>
               <Route path="/" element={<DisplayFood />} />
               <Route path="/profile" element={<userProfile />} />
          </Routes>
      </Router>
    </React.StrictMode>
  </Provider>,
  document.getElementById('root'),
);

From the code snippet above, you will notice how the provider component accepts our store as a prop. With this setup, our store is now ready to be accessed everywhere in our application.

Access and make changes to data in the store

Now, data can be accessed in the DisplayFood and userProfile components directly from the store without depending on each other. Recall that the useSelector hook serves as the delivery agent that always fetches the current state of pizza to the component once there is any change while the useDispatch serves as the call attendant who is even ready to take your order and pass it to the chef.

DisplayFood.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { preparePizza } from '../manager';

const DisplayFood = () => {
    const dispatch = useDispatch();
    const foodStore = useSelector((state) => state);

    const orderPizza = (pizzaType) => {
         dispatch(preparePizza(pizzaType));

// when this function is executed, the dispatch redux function
// supplies the reducer (manager) under the hood the
// corresponding action necessary to make create the pepperoni pizza
     }

    return (
        <div> 
           <h2>Ordered Junk Food</h2>
            <div> Pizza Delivered: { foodStore.pizza } </div>
            <button onClick= { () => orderJunk('pepperoni') }>Order Pizza</button>
         </div>
      );
}

export default DisplayFood;
userProfile.js

import React from 'react';
import { useSelector } from 'react-redux';

const userProfile = () => {
    const { pizza, drinks, soup } = useSelector((state) => state);

    // using object destructuring to get data from the state object

    return (
        <div> 
           <h2>This is my user profile</h2>
            <div> Junk Food: { pizza } </div>
            <div> Drinks: { drinks } </div>
           <div> Soup: { soup } </div>
         </div>
      );
}

export default userProfile;

Conclusion

This article focuses on giving you a head start on react-redux by breaking down the complex terms in relation to real-world experience, therefore there are a few concepts that are not covered in detail which I would discuss in my next articles. I will hint at a few here:

  1. Combined reducers: This is a redux function that is used when you have multiple reducers or managers that manage different aspects of the store.
  2. Asynchronous actions: Actions that have side effects
  3. Middleware (thunks): Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more. See Redux middleware

There are other centralization techniques for managing data across react components like the context api.

I hope you have found this informative and I would like to thank you for reading.

Feel free to check out a project, I just completed using react-redux on Github

Stock Metrics App, you would see how redux was used to manage the central data store. Would appreciate a star on the project.