Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Deciding When to Connect Your Component to the Redux Store

Jun 21, 2019 • 16 Minute Read

Introduction

Redux is my go-to state management tool, when building a web app for scaling. Regardless of the hassle one has to go through initially, with a large enough codebase, everyone comes to a point where they’re thanking Redux for saving the day. A major headache of having Redux is the task of connecting your components to the Redux store and actions. Although it is often preferred to keep all Redux bindings at one place (usually the top most component), the practice soon gets messy with complex component hierarchies.

ToDo++

For this exercise, we will be building an extended version of the notorious ToDo app. While the boring ToDo apps simply have one type of item, ie: text items, our ToDo++ items can be a/an

  • text item
  • checklist item
  • item with actions
  • mix of the above

With this requirement in mind, let's initiate our design process with component decomposition. Firstly, we know that we only have one page, AppComponent, where we will show our items. But, as good front-end engineers, we always anticipate for later changes, so we would encapsulate our ToDo list functions in a new component and segregate the component from the page. ViewPageComponent will be the one and only page in our app. Just putting it there will prevent the confusion of having everything inside the App.js. So, our initial structure looks like:

      AppComponent
    |- ViewPageComponent
        |- ToDoListComponent
    

What does a TodoList consists of? ToDoItems, of course. Although there's only four ToDo item types, at the moment, we would like to keep space for future additions. So, we decouple the items from the list. Then, we further decompose the item into atomic components, taking the idea from the requirements above.

      AppComponent
    |- ViewPageComponent
        |- ToDoListComponent
            |- ToDoItemComponent
                |- TextComponent
                |- CheckComponent
                |- ButtonComponent
    

Note The relationship between the ToDoItemComponent and the atomic components is not inheritance but aggregation. The reason for this is that each ToDoItem will contain one or more of the atomic components, rather than just being one of the components.

Now that the component breakdown is completed, let's dive into implementation.

The Boilerplate

If you are too bored to setup the React-Redux project from scratch, you can go ahead and clone the code from the GitHub repo.

For the rest, let's start off by creating a new React app using the React CLI. In your terminal, generate the app with following command:

      $ create-react-app todoplus
$ cd todoplus
    

Next, we add Redux to our project:

      $ npm install --save redux react-redux
    

Once done, we create the page component and initialize the store, actions, and reducer for Redux. The directory structure for source is as follows:

      todoplus
    |- src
        |- ToDoListComponent
            |- ToDoItemComponent.js
            |- TextComponent.js
            |- CheckComponent.js
            |- ButtonComponent.js
            |- index.js
        |- actions.js
        |- App.css
        |- App.js
        |- index.css
        |- index.js
        |- reducers.js
        |- store.js
    
      // actions.js

// These are our action types
export const DELETE_ITEM = "DELETE_ITEM"
export const PRINT_ITEM = "PRINT_ITEM"
export const TOGGLE_CHECK = "TOGGLE_CHECK"


// Now we define actions
export function deleteItem(itemIndex){
    return {
        type: DELETE_ITEM,
        itemIndex
    }
}

export function printItem(itemIndex){
    return {
        type: PRINT_ITEM,
        itemIndex
    }
}

export function togglCheck(itemIndex){
    return {
        type: TOGGLE_CHECK,
        itemIndex
    }
}
    
      // reducers.js

import { DELETE_ITEM, PRINT_ITEM, TOGGLE_CHECK } from './actions';

const initialState = {
    tasks: [
        {
            label: "Task 01",
            isCheckItem: false,
            hasActions: false,
            isChecked: false
        },
        {
            label: "Task 02",
            isCheckItem: true,
            hasActions: false,
            isChecked: true
        },
        {
            label: "Task 03",
            isCheckItem: false,
            hasActions: true,
            isChecked: false
        },
        {
            label: "Task 04",
            isCheckItem: true,
            hasActions: true,
            isChecked: false
        }
    ]
}

export default function todoReducer(state, action) {
    if (typeof state === 'undefined') {
        return initialState
    }

    switch(action.type){
        case DELETE_ITEM:
            console.log("deleting");
            break;
        
        case EDIT_ITEM:
            console.log("editing");
            break;
        
        case TOGGLE_CHECK:  
            var items = [...state.tasks];
            items[action.itemIndex].isChecked = !items[action.itemIndex].isChecked;
            state = {...state, tasks: items};
    }

    // We create an empty reducer for now

    return state
}
    
      // store.js

import { createStore } from 'redux'
import todoReducer from './reducers'

export default createStore(todoReducer)
    
      // ViewPageComponent.js

import React, { Component } from 'react';
import { connect } from 'react-redux'
import TodoListComponent from './TodoListComponent';
import { deleteItem, printItem, togglCheck } from './actions';

class ViewPageComponent extends Component {
  render(){
    return (
        <TodoListComponent 
          itemList={this.props.items} 
        />
    );
  }
}

function mapStateToProps(state) {
  return {
      items: state.tasks
  }
}

const mapDispatchToProps = {
  deleteItem,
  printItem,
  togglCheck
};

export default connect(mapStateToProps, mapDispatchToProps)(ViewPageComponent);
    
      // App.js

import React from 'react';
import { Provider } from 'react-redux'
import store from './store';
import ViewPageComponent from './ViewPageComponent';

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

export default App;
    

Note that the ViewPageComponent is already connected to the Redux store and actions. Let's take a moment to analyse the data structure we used in the reducers.

For the context of this guide, we assume that all data has already been fetched and stored in the tasks state in the reducer. Further, the app will only show the tasks and will not handle inserting new tasks to the list.

Observing the tasks array in reducers.js, we can identify the four types of tasks we have to render from our component:,

  1. Task 01 - text only item
  2. Task 02 - checkbox item
  3. Task 03 - text item with action buttons
  4. Task 04 - checkbox item with action buttons

Also we pre-defined two actions:

  1. Delete Item action - this will remove the current items from the task list
  2. Print Item action - this will simply print the task text to console out

With these in mind, let's continue in our journey.

Building the Text-only Todo

The first version of the app will display "text" items only. This will let us build the core components of the app which then can be extended. We wil start with TextComponent. Right now, we need to determine if this component needs direct access to the store. Going with the traditional thought process, we will model TextComponent as a dumb component and will pass any property required to dsiplay content using props.

Note
The traditional thought process around connecting with redux store has two general pieces of advice (1) Minimize the spread of Redux connectivity in the app, ie: to concentrate on a few components when possible; (2) To give the connectivity to components higher in the hierarchy. Both of these are helpful in general, except when the situation requires more finesse.

      export default function TextComponent({ itemId, itemText }){
    return (
        <div className="list-item-text">
            <span>{itemText}</span>
        </div>
    )
}
    

With that done, we will create the ToDoItemComponent. Again, we will be going with the strategy that the store connectivity is to be in the highest level. So, any property is to be passed through props.

      import TextComponent from "./TextComponent";

export default function({ item }){
    return (
        <div className="list-item">
            <TextComponent 
                itemId={item.id} 
                itemText={item.label}    
            />
        </div>
    )
}
    

Finally, we create the TodoListComponent (in the index.js). Still, the component won't be interacting directly with the React store or actions. The necessary information, ie: the list of items, will be passed from the parent component. Also, we will add this component to the ViewPageComponent and pass the item list to it, so we can verify that everything works up to that point.

      // TodoListComponent/index.js

import React from 'react';
import ToDoItemComponent from "./ToDoItemComponent";

export default function TodoListComponent({itemList}){
    return (
        <div className="todo-list">
            {itemList.map((item, idx) => (
                <ToDoItemComponent item={item} key={idx} />
            ))}
        </div>
    )
}
    
      //ViewPageComponent.js

// ...

class ViewPageComponent extends Component {
  render(){
    return (
        <TodoListComponent itemList={this.props.items} />
    );
  }
}

// ...
    

If you run it and test, it should show the items. All good.

Extending Todo: Adding checkbox Item

Next, we will add checkbox items. This should be easy. First, we create the CheckComponent and then modify the TodoItemComponent to reflect the check items. Now the problem arises, where should we put the checkbox toggle action? Let's go with the current pattern and pass on the toggle action right from the highest in the hierarchy: ViewPageComponent.

      // CheckComponent.js

import React from 'react';

export default function CheckComponent({itemId, itemText, isChecked, toggleItem}){
    return (
        <div className="list-item list-item-text">
            <input 
                type="checkbox" 
                onChange={() => toggleItem(itemId)} 
                checked={isChecked} 
            />
            <span>{itemText}</span>
        </div>
    )
}
    
      //TodoItemComponent.js

import React from 'react';
import TextComponent from "./TextComponent";
import CheckComponent from './CheckComponent';

export default function({ item, itemId, togglCheck }){
    return (
        <div className="list-item">
            { item.isCheckItem ? (
                <CheckComponent
                    itemId={itemId}
                    itemText={item.label}
                    isChecked={item.isChecked}
                    toggleItem={togglCheck}
                />
            ) : (
                <TextComponent 
                    itemId={itemId} 
                    itemText={item.label}    
                />
            )}
        </div>
    )
}
    
      // index.js

import React from 'react';
import ToDoItemComponent from "./ToDoItemComponent";

export default function TodoListComponent({itemList, togglCheck}){
    return (
        <div className="todo-list">
            {itemList.map((item, idx) => (
                <ToDoItemComponent 
                    item={item} 
                    itemId={idx}
                    key={idx} 
                    togglCheck={togglCheck}
                />
            ))}
        </div>
    )
}
    
      //ViewPageComponent.js

// ...
class ViewPageComponent extends Component {
  render(){
    return (
        <TodoListComponent 
          itemList={this.props.items} 
          togglCheck={this.props.togglCheck}
        />
    );
  }
}
// ...
    

As we can quickly observe, to add just the CheckComponent to the hierarchy, we had to create changes in three other components. The reason being, to pass on the actions from the top most component to the lowest component. The toggleCheck action is passed from ViewPageComponent to TodoListComponent to TodoItemComponent and, finally. to CheckComponent to be used.

Let's take a moment and think about if this is scalable in the future. We are yet to add the action button components, and this would require adding changes to three other layers in the hierarchy. At this point, we need to make a decision about changing our architecture to better adapt to changes.

Thinking Process

Moving Actions to CheckComponent

We'll start thinking from the very bottom layer. What if we connect the CheckComponent directly to the store? Then, once we create the ButtonComponent, we would not need to modify two other unnecessary files. But the focus is to make it robust, overall. This is not a bad option, but it lowers the overall maintainability. Having Redux connections at one point is preferred, if possible. Having each component connecting itself to Redux will reduce adaptability in the long run.

Moving Actions to TodoItemComponent

In comparison to the previous option, this is an improvement. Every new component doesn't need to be connected to the store and TodoItemComponent can act as the intermediate. Still, it will require a component higher in the hierarchy to retrieve the task list from the store, which means another component needs to be connected to the store. So, we must brainstorm further.

Moving Actions and Store Access to TodoListComponent

If we simply move all Redux connectivities to TodoListComponent, it could retrieve the task list, rather than getting it through the ViewPageComponent, and only will need to remove one layer for modifications when passing the actions to the bottom layer component. This seems like the ideal choice, yet it still requires passing down actions several layers below to bottom-level components.

Hybrid Method: Segregating the redux Connectivities

A hybrid method seems ideal for the scenario. We could move the store access to TodoListComponent while keeping the action connectivities at TodoItemComponent. This essentially creates two places where Redux connections occur, but it retains the overall maintainability and makes it easy to scale. Let's change the overall code to reflect this change.

The completed code is available in the Github repo (since the code is repetitive work, it is not added into the guide). Take time to read through the code to observe the benefits gained by moving to our hybrid architecture.

Conclusion

An important update on this matter is the introduction of Redux Hooks, in the recent release of Redux. Hopefully the adaption will be explained in a future guide.

In conclusion, the key takeaway is that structuring your component hierarchy and the Redux connectivities is a subjective matter. There will be no concrete rules to select the perfect option. But in general, following the rules of traditional pattern will give you a better foundation to start.