If you're trying to pick a library to perform state management for your React application, chances are you're going to end up picking Redux. But Redux comes with quite a steep learning curve. Or does it? In this article, we'll learn Redux, without actually using Redux.


Fry Not Sure

It's 2018. If you are writing a front-end application, it's highly likely you'll be doing it using React. And while React's built-in setState might be good enough for a small application, it's not going to cut it when your application grows bigger, and your application's state get's increasingly complex. Thankfully, there are quite a few options for state management when it comes to React applications.

The most popular of these state management solutions for React applications, is Redux. While the core concepts behind Redux are quite simple, the actual implementation of it using the Redux libraries and it's myriad helpers tends to throw people off, due to it's steep learning curve.

To better understand Redux, and to bring out the simplicity hidden behind all the boilerplate, I'm going to be writing an application in vanilla React, without using Redux. I'll then introduce Redux concepts, and modify the application to use these concepts. Note that I'll only using the concepts, not the actual library itself.


The Test Subject: Yet Another Todo Application

If you've been reading React tutorials, you're probably sick and tired of todo apps. Unfortunately, a todo app is just about complicated enough to demonstrate Redux concepts, while not requiring an actual backend API, so you're stuck with another Todo sample application. However, I'm going to make it a tad bit more complicated, so we can better translate Redux ideas onto the application.

So our todo application is going to look something like this:

Todo Screenshot

As you can see, the main view of the application is split into two parts:

  • Todo List View - This section displays a list of available todo items, a button to add a new todo item, and buttons to show/hide todo items based on whether they are fully or partially completed.
  • Todo Detail View - When clicking on a todo item in the list view, or on the Add Todo button, this section displays the todo item, and the various tasks associated with that todo item. From here, the todo item and it's associated tasks can be edited, as well as marked as done/not done. When a todo item is marked as done here, and saved, it is also marked as done in the sidebar.

To build this application, let us first split our views into the various components that we will be using. When properly split up, the components used look like this:

Todo Screenshot Components


Now let's have a look at the code required to build these components.

[Update January 2019] The complete code for this application has been made available at Github. You can checkout different commits, to see the code changes in the application, as we progress through the article.

The first component, at the root of the application is the App component. This is a container component which will load the data required for the app, manage any state that is shared between components, and drill the required properties through to it's child components. The AppContainer.js component looks like this:

/**
 * App Root Container Component
 */

import React, { Component } from 'react';

import './AppContainer.scss';

import Sidebar from '../Sidebar';
import Content from '../Content';
import { generateID } from '../../utils';

class App extends Component {
    constructor(props) {
        super(props);

        this.state = {
            todoList: []
        };
    }

    addTodo = () => {
        this.setState({
            expandedTodo: {
                id: generateID()
            },
            expandedTodoEditable: true
        });
    }

    saveTodo = (todo) => {
        let todoIndex = this.state.todoList.findIndex(todoItem => todoItem.id === todo.id);
        todoIndex = todoIndex === -1 ? this.state.todoList.length : todoIndex;

        const todoList = [...this.state.todoList.slice(0, todoIndex), todo, ...this.state.todoList.slice(todoIndex + 1)];
        this.setState({
            todoList,
            expandedTodo: todo,
            expandedTodoEditable: false
        });
    }

    openTodo = (todo) => {
        this.setState({ expandedTodo: todo, expandedTodoEditable: false });
    }

    renderExpandedTodo() {
        if (!this.state.expandedTodo)
            return null;

        return (
            <Content
                todo={this.state.expandedTodo}
                editable={this.state.expandedTodoEditable}
                saveTodo={this.saveTodo} />
        );
    }

    render() {
        const expandedTodo = this.renderExpandedTodo();

        const component = (
            <div
                className="app-container">
                <Sidebar
                    todoList={this.state.todoList}
                    openTodo={this.openTodo}
                    addTodo={this.addTodo} />
                {expandedTodo}
            </div>
        );

        return component;
    }
};

export default App;

The AppContainer component stores all the todo items in the todoList property of the component's state. In addition, two other state properties expandedTodo and expandedTodoEditable are also maintained to keep track of the todo item which is displayed in the Todo Detail view, and whether the todo is editable or not.

The addTodo, saveTodo and openTodo methods are all callback methods which are drilled down to child components via props. As you can see, the AppContainer component has two child components Content and Sidebar.

Sidebar component is responsible for rendering the Todo List View, and it is split into a container component called SidebarContainer and the presentation component Sidebar. SidebarContainer looks like this:

/**
 * Sidebar Container component
 */

import React, { Component } from 'react';

import Sidebar from './Sidebar';

class SidebarContainer extends Component {
    categorizeTodoList() {
        let { todoList = [] } = this.props;

        let completedTodoList = [];
        let partlyCompletedTodoList = [];
        let notStartedTodoList = [];

        todoList = todoList.map(todoItem => {
            const { tasks = [] } = todoItem;

            const completedTasksForThisTodoItem = tasks.filter(task => task.done);

            if (completedTasksForThisTodoItem.length === tasks.length) {
                todoItem = Object.assign({}, todoItem, { completionState: 'completed' });
                completedTodoList.push(todoItem);
            } else if (completedTasksForThisTodoItem.length === 0) {
                todoItem = Object.assign({}, todoItem, { completionState: 'notStarted' });
                notStartedTodoList.push(todoItem);
            } else {
                todoItem = Object.assign({}, todoItem, { completionState: 'partial' });
                partlyCompletedTodoList.push(todoItem);
            }

            return todoItem;
        });

        return { todoList, completedTodoList, partlyCompletedTodoList, notStartedTodoList };
    }

    render() {
        const {
            todoList,
            completedTodoList,
            partlyCompletedTodoList,
            notStartedTodoList
        } = this.categorizeTodoList();

        const component = (
            <Sidebar
                addTodo={this.props.addTodo}
                openTodo={this.props.openTodo}
                todoList={todoList}
                completedTodoList={completedTodoList}
                partlyCompletedTodoList={partlyCompletedTodoList}
                notStartedTodoList={notStartedTodoList} />
        );

        return component;
    }
};

export default SidebarContainer;

SidebarContainer is responsible for categorizing the todoList items as completedTodoList, partlyCompletedTodoList and notStartedTodoList. These are then passed down to the Sidebar presentation component as props. Sidebar component looks like this:

/**
 * Sidebar component
 */

import React, { Component } from 'react';

import './Sidebar.scss';
import TodoList from '../_shared/TodoList';

const headerPrefixByVisibilityFilter = {
    all: 'All',
    none: 'Not Started',
    partial: 'Partly Completed',
    done: 'Completed'
};

class Sidebar extends Component {
    constructor(props) {
        super(props);

        this.state = {
            visibilityFilter: 'all'
        };
    }

    setVisibilityFilter(filter) {
        return (ev) => {
            this.setState({ visibilityFilter: filter });
        };
    }

    renderHeader() {
        const component = (
            <div
                className="header">
                <span>{headerPrefixByVisibilityFilter[this.state.visibilityFilter]} Todos </span>
                <button
                    onClick={this.props.addTodo}>
                    Add Todo
                </button>
            </div>
        );

        return component;
    }

    renderFooter() {
        const component = (
            <div
                className="footer">
                <span>Show todos: </span>
                <button
                    onClick={this.setVisibilityFilter('all')} >
                    All
                </button>
                <button
                    onClick={this.setVisibilityFilter('none')} >
                    Not Started
                </button>
                <button
                    onClick={this.setVisibilityFilter('partial')} >
                    Partly Done
                </button>
                <button
                    onClick={this.setVisibilityFilter('done')} >
                    Completed
                </button>
            </div>
        );

        return component;
    }

    renderTodoList() {
        let todoListItems;

        if (this.state.visibilityFilter === 'all')
            todoListItems = this.props.todoList;
        else if (this.state.visibilityFilter === 'none')
            todoListItems = this.props.notStartedTodoList;
        else if (this.state.visibilityFilter === 'partial')
            todoListItems = this.props.partlyCompletedTodoList;
        else if (this.state.visibilityFilter === 'done')
            todoListItems = this.props.completedTodoList;

        const component = (
            <TodoList
                openTodo={this.props.openTodo}
                todoList={todoListItems} />
        );

        return component;
    }

    render() {
        const header = this.renderHeader();
        const footer = this.renderFooter();
        const todoList = this.renderTodoList();

        const component = (
            <div
                className="sidebar">
                {header}
                {todoList}
                {footer}
            </div>
        );

        return component;
    }
};

export default Sidebar;

The Sidebar component renders the header section which has the Add Todo button, the footer section which has the various visibility filter buttons, and the TodoList which displays all available todo items, using the TodoList component. The TodoList component looks like this:

/**
 * Todo list component
 */

import React, { Component } from 'react';

import TodoItem from '../TodoItem';

class TodoList extends Component {
    openTodo(todo) {
        return () => {
            this.props.openTodo(todo);
        };
    }

    renderTodoListChildren() {
        if (this.props.todoList.length === 0) {
            return (
                <div
                    className="message">
                    No Todos Added Yet.
                </div>
            );
        }

        const todoItems = (this.props.todoList).map(todoItem => {
            console.log(this.props.readOnly);
            return (
                <TodoItem
                    key={todoItem.id}
                    {...todoItem}
                    onClick={this.openTodo(todoItem)}
                    readOnly={true}
                    editable={this.props.editable}
                />
            );
        });

        return todoItems;
    }

    render() {
        const todoListChildren = this.renderTodoListChildren();

        const component = (
            <div
                className="todo-list">
                {todoListChildren}
            </div>
        );

        return component;
    }
};

export default TodoList;

The TodoList component in turn uses the TodoItem component to display the todo items. The TodoItem in the sidebar is displayed in readOnly mode, which hides the Edit and Save buttons, and also disables the checkbox to mark an item as done. TodoItem component looks like this:

/**
 * Todo Item Component
 */

import React, { Component } from 'react';

import CheckableItem from '../CheckableItem';
import { generateID } from '../../../utils';

class TodoItem extends Component {
    constructor(props) {
        super(props);

        this.state = this.getStateFromProps(props);
    }

    componentWillReceiveProps(nextProps) {
        this.setState(this.getStateFromProps(nextProps));
    }

    getStateFromProps(props) {
        const { description, tasks = [], done, id, readOnly, editable } = props;
        return {
            description,
            tasks,
            done,
            id,
            readOnly,
            editable
        };
    }

    onTaskChange(taskId) {
        return (fieldName, value) => {
            fieldName = fieldName === 'checked' ? 'done' : fieldName;

            let taskIndex = this.state.tasks.findIndex(taskItem => taskItem.id === taskId);
            taskIndex = taskIndex === -1 ? this.state.tasks.length : taskIndex;

            let task = Object.assign({}, (this.state.tasks[taskIndex] || {}), { id: taskId, [fieldName]: value });

            const tasks = [...this.state.tasks.slice(0, taskIndex), task, ...this.state.tasks.slice(taskIndex + 1)];

            const completedTasks = tasks.filter(task => task.done);
            const done = (completedTasks.length === tasks.length);

            this.setState({
                tasks,
                done
            });
        }
    }

    onTodoSave = () => {
        const { description, id, tasks } = this.state;

        const completedTasks = tasks.filter(task => task.done);
        const done = completedTasks.length === tasks.length;

        this.props.onSave({ description, id, done, tasks });
    }

    onTodoChange = (fieldName, value) => {
        fieldName = fieldName === 'checked' ? 'done' : fieldName;
        const tasks = this.state.tasks.map(taskItem => {
            return Object.assign({}, taskItem, { done: value });
        });

        this.setState({
            [fieldName]: value,
            tasks
        });
    }

    toggleEditable = () => {
        this.setState({ editable: !this.state.editable });
    }

    renderTaskItems() {
        debugger;
        if (!this.props.showTasks)
            return;

        const tasks = this.state.editable ? this.state.tasks.concat([{
            id: generateID()
        }]) : this.state.tasks;

        const taskItems = tasks.map(taskItem => {
            return (
                <CheckableItem
                    key={taskItem.id}
                    onChange={this.onTaskChange(taskItem.id)}
                    className="task-item"
                    checked={taskItem.done}
                    checkable={!this.state.editable}
                    description={taskItem.description}
                    readOnly={taskItem.readOnly || !this.state.editable} />
            );
        });

        return (
            <div
                className="task-list">
                <div
                    className="caption">
                    Tasks
                </div>
                {taskItems}
            </div>
        );
    }

    renderSaveButton() {
        if (this.state.readOnly)
            return;

        return (
            <button
                onClick={this.onTodoSave}>
                Save
            </button>
        );
    }

    renderEditButton() {
        if (this.state.readOnly)
            return;

        return (
            <button
                onClick={this.toggleEditable}>
                {this.state.editable ? 'Reset' : 'Edit'}
            </button>
        );
    }

    render() {
        const taskItems = this.renderTaskItems();
        const saveButton = this.renderSaveButton();
        const editButton = this.renderEditButton();

        const component = (
            <div
                className="todo-item">
                <CheckableItem
                    className="header"
                    onChange={this.onTodoChange}
                    onClick={this.props.onClick}
                    checked={this.state.done}
                    checkable={!this.state.readOnly}
                    description={this.state.description}
                    readOnly={this.state.readOnly&nbsp;|| !this.state.editable} />
                {taskItems}
                {editButton}
                {saveButton}
            </div>
        );

        return component;
    }
};

export default TodoItem;

The TodoItem component uses the CheckableItem component to render each todo's description and done checkbox, as well as to render each task's description and done checkbox. CheckableItem looks like this:

/**
 * CheckableItem Component
 */

import React, { Component } from 'react';

class CheckableItem extends Component {
    toggleChecked = () => {
        this.props.onChange('checked', !this.props.checked);
    }

    onChange(fieldName) {
        return (ev) => {
            const { value } = ev.currentTarget;
            this.props.onChange(fieldName, value);
        };
    }

    renderDescription() {
        if (this.props.readOnly) {
            return (
                <span
                    className="description">
                    {this.props.description}
                </span>
            );
        } else {
            return (
                <textarea
                    className="description"
                    onChange={this.onChange('description')}
                    placeholder={this.props.placeholder || ''}
                    value={this.props.description || ''} />
            );
        }
    }

    render() {
        const description = this.renderDescription();

        const component = (
            <div
                onClick={this.props.onClick}
                className={this.props.className || ''}>
                <input
                    type="checkbox"
                    onChange={this.toggleChecked}
                    disabled={!this.props.checkable}
                    checked={this.props.checked || false} />
                {description}
            </div>
        );

        return component;
    }
};

export default CheckableItem;

The CheckableItem component can be rendered in either editable or non-editable modes. In editable mode, the description text of the item can be changed. When we click the Edit button in the TodoItem component, the editable mode is toggled, and you can edit the description text. Clicking the Save button saves the TodoItem.

If we go back to the AppContainer component, we see that the other component used is the Content component. The Content component looks like this:

/**
 * Content Component
 */

import React, { Component } from 'react';

import TodoItem from '../_shared/TodoItem';

class Content extends Component {
    constructor(props) {
        super(props);

        this.state = {
            todo: this.props.todo
        };
    }

    componentWillReceiveProps(nextProps) {
        this.setState({ todo: nextProps.todo });
    }

    saveTodo = (todo) => {
        this.props.saveTodo(todo);
    }

    onTodoChange(fieldName) {
        return (ev) => {
            const { value } = ev.currentTarget;

            const todo = Object.assign({}, this.state.todo, {
                [fieldName]: value
            });

            this.setState({ todo });
        };
    }

    render() {
        const component = (
            <div
                className="content-container">
                <TodoItem
                    onSave={this.saveTodo}
                    {...this.state.todo}
                    showTasks={true}
                    editable={this.props.editable} />
            </div>
        );

        return component;
    }
};

export default Content;

The Content component uses the same TodoItem component to display the detailed view of the selected todo item. You'll notice that the TodoItem is passed the showTasks property. This causes the TodoItem component to render the tasks list along with the todo item. The onSave method is a callback method passed on to the TodoItem, which is triggered when the Save button is clicked on the TodoItem component.

If you're familiar with writing React application, the above code should be quite familiar and easy to understand for you. This is how you would write a React application with one-way data flow from parent to child components via props, and any data required to flow from child to parent, being sent via callback methods passed down from parent to child.

[Update January 2019] You can checkout the tag 'decentralized-state' on Github, to get the complete code until this point.


So what are the issues with our state management so far?

The biggest issue with our state management so far, is that we are managing our state as close to the component that renders it as possible. When starting out with React, this is generally a good and recommended approach, however, as your application grows bigger and more complex, it often turns out that we have bits and pieces of our state being managed all over the application.

This makes it harder to reason about the overall state of the application, especially when the state accessed by one component is dependant on the state changes made in another component. This is what happens in our Sidebar and Content components as well, since we need to manage the done state of each todo item, which can be changed in the Content component's children, and then the changes need to reflect in the Sidebar component. We are calculating whether a todo item is done or not based the status of the associated tasks being done or not, and since this information is required in multiple different places and we always need to display the most updated information we have, we always end up recalculating the done state of each todo item separately in both the Sidebar and the Container.

Another side-effect of having application state strewn over multiple components is that it becomes very difficult to serialize/deserialize the state. We will have to serialize/deserialize each state component individually, and ensure that they are restored correctly.

We can get rid of these problems quite easily by centralizing the state of our application. Basically instead of having application state managed by multiple components, we have a single application state object, and each component then takes the properties of the state object that are required by it, and it's child components to render their views. We then use prop drilling to pass the properties down through the various components levels, right down to the components that actually require them.

When a component takes certain props, which it doesn't use directly, but then passes on those props to a child component, for that child component to use, the parent component is said to be prop drilling. Prop drilling is fine for trivial applications, but can get quite messy, quite quickly for anything bigger. Remember that, we'll get back to it later.

So let's make the changes required to centralize all state into a single state object. I like to call this single object responsible for the entire application state as the God object.


Single Centralized State Object

We are going to be centralizing the state of the entire application in a single object, and in our example application, that object is going to be the state object of the AppContainer component. No other component will maintain it's own state, and instead the state maintained by each component will be rolled-up to their parent components, which will roll them up to their own parent components, and so on and so forth, until all the state is rolled up to the AppContainer component.

To do this, we need to make the following changes:

  • Replace state usage with props usage.
  • Pass along callback method in the props from parent to the child components, which will then use these callback methods to make changes to the state maintained in the centralized object i.e. the AppContainers state object.

Let's start off with the TodoItem component. Currently, the TodoItem component gets the initial description, done, and tasks properties from it's parent via props, copies them over to the state, and renders the TodoItem. When the TodoItem is edited, the component stores any changes to these properties in it's own state, and when the Save button is clicked, these changes are used to build a new todo item, which is then passed along to the callback method passed in via the onSave prop.

To remove state from the TodoItem component, we will no longer be using state to track changes to the todo item. Instead, whenever a todo item is changed, we will call the onChange callback method passed in from the parent component via props. Since we are no longer using state to track the changes to the todo item, we no longer have to copy the initial description, done, and tasks properties from the props either. Once we make the require changes, TodoItem looks like this:

/**
 * Todo Item Component
 */

import React, { Component } from 'react';

import CheckableItem from '../CheckableItem';
import { generateID } from '../../../utils';

class TodoItem extends Component {
    static get defaultProps() {
        return {
            tasks: []
        };
    }

    onTaskChange = (taskId, fieldName, value) => {
        debugger;
        fieldName = fieldName === 'checked' ? 'done' : fieldName;

        let taskIndex = this.props.tasks.findIndex(taskItem => taskItem.id === taskId);
        taskIndex = taskIndex === -1 ? this.props.tasks.length : taskIndex;

        let task = Object.assign({}, (this.props.tasks[taskIndex] || {}), { id: taskId, [fieldName]: value });

        const tasks = [...this.props.tasks.slice(0, taskIndex), task, ...this.props.tasks.slice(taskIndex + 1)];

        const completedTasks = tasks.filter(task => task.done);
        const done = (completedTasks.length === tasks.length);

        this.props.onChange({
            tasks,
            done
        });
    }

    onTodoChange = (fieldName, value) => {
        fieldName = fieldName === 'checked' ? 'done' : fieldName;

        let tasks = this.props.tasks;

        if (fieldName === 'done') {
            tasks = this.props.tasks.map(taskItem => {
                return Object.assign({}, taskItem, { done: value });
            });
        }

        this.props.onChange({
            [fieldName]: value,
            tasks
        });
    }

    renderTaskItems() {
        if (!this.props.showTasks)
            return;

        const tasks = this.props.editable ? this.props.tasks.concat([{
            id: generateID()
        }]) : this.props.tasks;

        const taskItems = tasks.map(taskItem => {
            return (
                <CheckableItem
                    key={taskItem.id}
                    onChange={this.onTaskChange.bind(null, taskItem.id)}
                    className="task-item"
                    checked={taskItem.done}
                    checkable={!this.props.editable}
                    description={taskItem.description}
                    readOnly={taskItem.readOnly || !this.props.editable} />
            );
        });

        return (
            <div
                className="task-list">
                <div
                    className="caption">
                    Tasks
                </div>
                {taskItems}
            </div>
        );
    }

    renderSaveButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={this.props.onSave}>
                Save
            </button>
        );
    }

    renderEditButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={this.props.toggleEditable}>
                {this.props.editable ? 'Reset' : 'Edit'}
            </button>
        );
    }

    render() {
        const taskItems = this.renderTaskItems();
        const saveButton = this.renderSaveButton();
        const editButton = this.renderEditButton();

        const component = (
            <div
                className="todo-item">
                <CheckableItem
                    className="header"
                    onChange={this.onTodoChange}
                    onClick={this.props.onClick}
                    checked={this.props.done}
                    checkable={!this.props.readOnly}
                    description={this.props.description}
                    readOnly={this.props.readOnly&nbsp;|| !this.props.editable} />
                {taskItems}
                {editButton}
                {saveButton}
            </div>
        );

        return component;
    }
};

export default TodoItem;

Next let's take a look at the TodoList component. As it so happens, the TodoList component does not actually maintain any state of it's own. So no changes are required here.

Let's look at the Content component next. This component does use state to manage the changes to the todo item, currently displayed in the detail view. Like we did for TodoItem component, we will be removing the state dependency and instead rolling up these to the parent component's props. Once we make the required changes, the Content component becomes a simple container component, which simply passes through the props from AppContainer to TodoItem. At this point, we could remove Content component and instead use the TodoItem component directly, but for the sake of organizing our project a bit better let's retain the Content component.

The Content component, after making the changes to remove state looks like this:

/**
 * Content Component
 */

import React, { Component } from 'react';

import TodoItem from '../_shared/TodoItem';

class Content extends Component {
    render() {
        const component = (
            <div
                className="content-container">
                <TodoItem
                    onSave={this.props.saveTodo}
                    {...this.props.todo}
                    showTasks={true}
                    editable={this.props.editable}
                    onChange={this.props.onTodoChange}
                    toggleEditable={this.props.toggleEditable} />
            </div>
        );

        return component;
    }
};

export default Content;

Next let's look at the Sidebar component. The only state maintained by the sidebar component is the visibilityFilter, which determines which list of todos to show: completed, partially completed, not yet started or all. Once we make the change to maintain the state of the visibilityFilter in the AppContainer component, and read it via props in the Sidebar component, the Sidebar component looks like this:

/**
 * Sidebar component
 */

import React, { Component } from 'react';

import './Sidebar.scss';
import TodoList from '../_shared/TodoList';

const headerPrefixByVisibilityFilter = {
    all: 'All',
    none: 'Not Started',
    partial: 'Partly Completed',
    done: 'Completed'
};

class Sidebar extends Component {
    renderHeader() {
        const component = (
            <div
                className="header">
                <span>{headerPrefixByVisibilityFilter[this.props.visibilityFilter]} Todos </span>
                <button
                    onClick={this.props.addTodo}>
                    Add Todo
                </button>
            </div>
        );

        return component;
    }

    renderFooter() {
        const component = (
            <div
                className="footer">
                <span>Show todos: </span>
                <button
                    onClick={this.props.setVisibilityFilter.bind(null, 'all')} >
                    All
                </button>
                <button
                    onClick={this.props.setVisibilityFilter.bind(null, 'none')} >
                    Not Started
                </button>
                <button
                    onClick={this.props.setVisibilityFilter.bind(null, 'partial')} >
                    Partly Done
                </button>
                <button
                    onClick={this.props.setVisibilityFilter.bind(null, 'done')} >
                    Completed
                </button>
            </div>
        );

        return component;
    }

    renderTodoList() {
        let todoListItems;

        if (this.props.visibilityFilter === 'all')
            todoListItems = this.props.todoList;
        else if (this.props.visibilityFilter === 'none')
            todoListItems = this.props.notStartedTodoList;
        else if (this.props.visibilityFilter === 'partial')
            todoListItems = this.props.partlyCompletedTodoList;
        else if (this.props.visibilityFilter === 'done')
            todoListItems = this.props.completedTodoList;

        const component = (
            <TodoList
                openTodo={this.props.openTodo}
                todoList={todoListItems} />
        );

        return component;
    }

    render() {
        const header = this.renderHeader();
        const footer = this.renderFooter();
        const todoList = this.renderTodoList();

        const component = (
            <div
                className="sidebar">
                {header}
                {todoList}
                {footer}
            </div>
        );

        return component;
    }
};

export default Sidebar;

As you can see, pretty much all the methods left in the Sidebar component perform some kind of rendering action, and no state is being maintained here.

Let's look at the SidebarContainer component next. This component does not actually maintain any state within itself, but we do need to make some changes to drill a few callback methods through to the child components via the SidebarContainer component. Specifically we are drilling two new props, setVisibilityFilter and visibilityFilter through to the Sidebar component. Once we've made the required changes, SidebarContainer looks like this:

/**
 * Sidebar Container component
 */

import React, { Component } from 'react';

import Sidebar from './Sidebar';

class SidebarContainer extends Component {
    categorizeTodoList() {
        let { todoList = [] } = this.props;

        let completedTodoList = [];
        let partlyCompletedTodoList = [];
        let notStartedTodoList = [];

        todoList = todoList.map(todoItem => {
            const { tasks = [] } = todoItem;

            const completedTasksForThisTodoItem = tasks.filter(task => task.done);

            if (completedTasksForThisTodoItem.length === tasks.length) {
                todoItem = Object.assign({}, todoItem, { completionState: 'completed' });
                completedTodoList.push(todoItem);
            } else if (completedTasksForThisTodoItem.length === 0) {
                todoItem = Object.assign({}, todoItem, { completionState: 'notStarted' });
                notStartedTodoList.push(todoItem);
            } else {
                todoItem = Object.assign({}, todoItem, { completionState: 'partial' });
                partlyCompletedTodoList.push(todoItem);
            }

            return todoItem;
        });

        return { todoList, completedTodoList, partlyCompletedTodoList, notStartedTodoList };
    }

    render() {
        const {
            todoList,
            completedTodoList,
            partlyCompletedTodoList,
            notStartedTodoList
        } = this.categorizeTodoList();

        const component = (
            <Sidebar
                addTodo={this.props.addTodo}
                openTodo={this.props.openTodo}
                todoList={todoList}
                completedTodoList={completedTodoList}
                partlyCompletedTodoList={partlyCompletedTodoList}
                notStartedTodoList={notStartedTodoList}
                setVisibilityFilter={this.props.setVisibilityFilter}
                visibilityFilter={this.props.visibilityFilter} />
        );

        return component;
    }
};

export default SidebarContainer;

The final component we need to change to make all this work is of course AppContainer which will maintain all the state that was being maintained across all the above components. Once we modify AppContainer to keep track of the state maintained in the child components previously, and create methods to change that state, and drill these methods and state properties to the child components, AppContainer looks like this:

/**
 * App Root Container Component
 */

import React, { Component } from 'react';

import './AppContainer.scss';

import Sidebar from '../Sidebar';
import Content from '../Content';
import { generateID } from '../../utils';

class App extends Component {
    constructor(props) {
        super(props);

        this.state = {
            todoList: [],
            visibilityFilter: 'all'
        };
    }

    addTodo = () => {
        this.setState({
            expandedTodo: {
                id: generateID()
            },
            expandedTodoEditable: true
        });
    }

    saveTodo = () => {
        const todo = this.state.expandedTodo;

        let todoIndex = this.state.todoList.findIndex(todoItem => todoItem.id === todo.id);
        todoIndex = todoIndex === -1 ? this.state.todoList.length : todoIndex;

        const todoList = [...this.state.todoList.slice(0, todoIndex), todo, ...this.state.todoList.slice(todoIndex + 1)];
        this.setState({
            todoList,
            expandedTodo: todo,
            expandedTodoEditable: false
        });
    }

    openTodo = (todo) => {
        this.setState({ expandedTodo: todo, expandedTodoEditable: false });
    }

    setVisibilityFilter = (visibilityFilter) => {
        this.setState({ visibilityFilter });
    }

    onTodoChange = (todoDiff) => {
        const expandedTodo = Object.assign({}, this.state.expandedTodo, todoDiff);
        this.setState({ expandedTodo });
    }

    toggleEditable = () => {
        this.setState({ expandedTodoEditable: !this.state.expandedTodoEditable });
    }

    renderExpandedTodo() {
        if (!this.state.expandedTodo)
            return null;

        return (
            <Content
                todo={this.state.expandedTodo}
                editable={this.state.expandedTodoEditable}
                saveTodo={this.saveTodo}
                onTodoChange={this.onTodoChange}
                toggleEditable={this.toggleEditable} />
        );
    }

    render() {
        const expandedTodo = this.renderExpandedTodo();

        const component = (
            <div
                className="app-container">
                <Sidebar
                    todoList={this.state.todoList}
                    openTodo={this.openTodo}
                    addTodo={this.addTodo}
                    setVisibilityFilter={this.setVisibilityFilter}
                    visibilityFilter={this.state.visibilityFilter} />
                {expandedTodo}
            </div>
        );

        return component;
    }
};

export default App;

And with that we have succesfully moved all state to a single god object, which in this case is the state object of the AppContainer component. If you run the application again, you'll find no changes in the application behaviour, but now all of our state is consolidated in one place. So now, if for example, we wanted to save our state to the browser's localStorage and restore it whenever the page is reloaded, we just need to serialize the state object of the AppContainer component to JSON, and save it.

[Update January 2019] You can checkout the tag 'centralized-state' on Github, to get the complete code until this point.


Centralized State Management

So far, all we have done is centralize the state that was previously maintained in multiple different components, into one single object. However, the actual management of that state, i.e. the transitions of state are still performed by individual components. The only difference is that previously, state transitions were performed by the component calling setState, whereas now, the component calls the callback method drilled down from the AppContainer.

So if we want to figure out how or why a particular state transition happened, we will have to go digging through all the components, figure out which component performs that state transition, and monitor that component. There may even be multiple components which trigger the same state transition, which only makes debugging any issues that much more complicated.

To get around this, not only do we need to use a single centralized state object, we also need to use centralized state transitions. Before we centralize the state management, let's extract that centralized state object from AppContainer's state object to an independant object of it's own. This will make it easier for us to perform transitions on it without having to worry about triggering React re-renders. Let's call this extracted state object as Store.

First, let's create a store.js file with the following content:

/**
 * Global State Store
 */

let state = {
    todoList: [],
    visibilityFilter: 'all',
    expandedTodo: null,
    expandedTodoEditable: false
};

function transitionState(stateDiff) {
    state = Object.assign({}, state, stateDiff);
    return state;
};

function getState() {
    return state;
};

export { transitionState, getState };

The store exports two methods:

  • transitionState - Takes an object as input and merges it with the pre-existing state to produce a new state, which is returned.
  • getState - Returns the current state.

Now let's modify AppContainer to make use of this store:

/**
 * App Root Container Component
 */

import React, { Component } from 'react';

import './AppContainer.scss';

import Sidebar from '../Sidebar';
import Content from '../Content';
import { generateID } from '../../utils';
import { transitionState, getState } from '../../state/store';

class App extends Component {
    constructor(props) {
        super(props);

        this.state = getState();
    }

    addTodo = () => {
        const newState = transitionState({
            expandedTodo: {
                id: generateID()
            },
            expandedTodoEditable: true
        });

        this.setState(newState);
    }

    saveTodo = () => {
        const state = getState();
        const todo = state.expandedTodo;

        let todoIndex = state.todoList.findIndex(todoItem => todoItem.id === todo.id);
        todoIndex = todoIndex === -1 ? state.todoList.length : todoIndex;

        const todoList = [...state.todoList.slice(0, todoIndex), todo, ...state.todoList.slice(todoIndex + 1)];
        const newState = transitionState({
            todoList,
            expandedTodo: todo,
            expandedTodoEditable: false
        });

        this.setState(newState);
    }

    openTodo = (todo) => {
        const newState = transitionState({ expandedTodo: todo, expandedTodoEditable: false })
        this.setState(newState);
    }

    setVisibilityFilter = (visibilityFilter) => {
        const newState = transitionState({ visibilityFilter });
        this.setState(newState);
    }

    onTodoChange = (todoDiff) => {
        const state = getState();
        const expandedTodo = Object.assign({}, state.expandedTodo, todoDiff);
        const newState = transitionState({ expandedTodo })
        this.setState(newState);
    }

    toggleEditable = () => {
        const state = getState();
        const newState = transitionState({ expandedTodoEditable: !state.expandedTodoEditable });
        this.setState(newState);
    }

    renderExpandedTodo() {
        if (!this.state.expandedTodo)
            return null;

        return (
            <Content
                todo={this.state.expandedTodo}
                editable={this.state.expandedTodoEditable}
                saveTodo={this.saveTodo}
                onTodoChange={this.onTodoChange}
                toggleEditable={this.toggleEditable} />
        );
    }

    render() {
        const expandedTodo = this.renderExpandedTodo();

        const component = (
            <div
                className="app-container">
                <Sidebar
                    todoList={this.state.todoList}
                    openTodo={this.openTodo}
                    addTodo={this.addTodo}
                    setVisibilityFilter={this.setVisibilityFilter}
                    visibilityFilter={this.state.visibilityFilter} />
                {expandedTodo}
            </div>
        );

        return component;
    }
};

export default App;

So what we've done is, we've basically changed all the setState calls to transitionState, which then gives us a newState which we set as AppContainer's state using setState. If we need to get the current state to get any data from it, we use getState instead of this.state.

Now you'll notice that by doing this, rather than make our code simpler, we've actually made it more complex. What we previously did with a single setState call, now requires a call to both transitionState and setState. Well, it is indeed more complicated than it needs to be, but fear not! That is a temporary state, and by the time we get to the end of this article, it will be simpler.

Another thing you might have noticed is that all the heavy lifting of performing the actual state transition, is actually still tied into each individual component. All transitionState does is merge the given differential state object with the pre-existing one. However, our objective is to reduce the unpredictability surrounding the generation of the differential state.

So we are now going to centralize the state transitions, i.e. the generation of the differential state object within our newly created store. Any component that needs to trigger a state transition, will simply provide the name of the transition and the data required for the transition to the transitionState method. The transitionState method will then perform the appropriate transformations neccessary to convert the given data into a differential state object, which is then merged with the pre-existing state.

Let's first rewrite transitionState to accomplish this:

/**
 * Global State Store
 */

let state = {
    todoList: [],
    visibilityFilter: 'all',
    expandedTodo: null,
    expandedTodoEditable: false
};

let transitionCompleteHandler;

function getState() {
    return state;
};

const transforms = {
    taskChange: ({ taskId, fieldName, value }) => {
        fieldName = fieldName === 'checked' ? 'done' : fieldName;

        const expandedTodoTasks = state.expandedTodo.tasks || [];

        let taskIndex = expandedTodoTasks.findIndex(taskItem => taskItem.id === taskId);
        taskIndex = taskIndex === -1 ? expandedTodoTasks.length : taskIndex;

        let task = Object.assign({}, (expandedTodoTasks[taskIndex] || {}), { id: taskId, [fieldName]: value });

        const tasks = [...expandedTodoTasks.slice(0, taskIndex), task, ...expandedTodoTasks.slice(taskIndex + 1)];

        const completedTasks = tasks.filter(task => task.done);
        const done = (completedTasks.length === tasks.length);

        const expandedTodo = Object.assign({}, state.expandedTodo, {
            tasks,
            done
        });

        return { expandedTodo };
    },

    todoChange: ({ fieldName, value }) => {
        fieldName = fieldName === 'checked' ? 'done' : fieldName;

        let tasks = state.expandedTodo.tasks;

        if (fieldName === 'done') {
            tasks = tasks.map(taskItem => {
                return Object.assign({}, taskItem, { done: value });
            });
        }

        const expandedTodo = Object.assign({}, state.expandedTodo, {
            [fieldName]: value,
            tasks
        });

        return { expandedTodo };
    },

    setExpandedTodo: ({ todo }) => {
        return { expandedTodo: todo, expandedTodoEditable: false };
    },

    addTodo: ({ id }) => {
        return {
            expandedTodo: { id },
            expandedTodoEditable: true
        };
    },

    saveExpandedTodo: () => {
        const todo = state.expandedTodo;

        let todoIndex = state.todoList.findIndex(todoItem => todoItem.id === todo.id);
        todoIndex = todoIndex === -1 ? state.todoList.length : todoIndex;

        const todoList = [...state.todoList.slice(0, todoIndex), todo, ...state.todoList.slice(todoIndex + 1)];
        return {
            todoList,
            expandedTodo: todo,
            expandedTodoEditable: false
        };
    },

    setVisibilityFilter: ({ visibilityFilter }) => {
        return { visibilityFilter };
    },

    toggleEditable: () => {
        return { expandedTodoEditable: !state.expandedTodoEditable };
    }
};

function transitionState(name, data) {
    const stateDiff = transforms[name](data);

    state = Object.assign({}, state, stateDiff);

    if (transitionCompleteHandler)
        transitionCompleteHandler(state);

    return state;
};

function onStateTransition(handler) {
    transitionCompleteHandler = handler;
};

export { transitionState, getState, onStateTransition };

As you can see, the transforms object contains a series of name, function pairs. When transitionState is called with a specific name, the corresponding transform function is invoked with the provided data. Every transform function returns the differential state object, which is then merged into the pre-existing state object by transitionState.

In addition, if any post state transition handler is registered, then transitionState also calls that handler and provides the newly transitioned state as a parameter. In order to register a post transition handler, store.js now also exports a new onStateTransition method.

Now let's see the changes required to the various components to use this centralized state transitions. First let's start off with TodoItem component. The complicated onTaskChange and onTodoChange logic have been moved to the corresponding transform functions, so all that onTaskChange and onTodoChange have to do inside TodoItem is to call the transitionState method.

/**
 * Todo Item Component
 */

import React, { Component } from 'react';

import CheckableItem from '../CheckableItem';
import { generateID } from '../../../utils';
import { transitionState } from '../../../state/store';

class TodoItem extends Component {
    static get defaultProps() {
        return {
            tasks: []
        };
    }

    onTaskChange = (taskId, fieldName, value) => {
        transitionState('taskChange', { taskId, fieldName, value });
    }

    onTodoChange = (fieldName, value) => {
        transitionState('todoChange', { fieldName, value });
    }

    renderTaskItems() {
        if (!this.props.showTasks)
            return;

        const tasks = this.props.editable ? this.props.tasks.concat([{
            id: generateID()
        }]) : this.props.tasks;

        const taskItems = tasks.map(taskItem => {
            return (
                <CheckableItem
                    key={taskItem.id}
                    onChange={this.onTaskChange.bind(null, taskItem.id)}
                    className="task-item"
                    checked={taskItem.done}
                    checkable={!this.props.editable}
                    description={taskItem.description}
                    readOnly={taskItem.readOnly || !this.props.editable} />
            );
        });

        return (
            <div
                className="task-list">
                <div
                    className="caption">
                    Tasks
                </div>
                {taskItems}
            </div>
        );
    }

    renderSaveButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={this.props.onSave}>
                Save
            </button>
        );
    }

    renderEditButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={this.props.toggleEditable}>
                {this.props.editable ? 'Reset' : 'Edit'}
            </button>
        );
    }

    render() {
        const taskItems = this.renderTaskItems();
        const saveButton = this.renderSaveButton();
        const editButton = this.renderEditButton();

        const component = (
            <div
                className="todo-item">
                <CheckableItem
                    className="header"
                    onChange={this.onTodoChange}
                    onClick={this.props.onClick}
                    checked={this.props.done}
                    checkable={!this.props.readOnly}
                    description={this.props.description}
                    readOnly={this.props.readOnly&nbsp;|| !this.props.editable} />
                {taskItems}
                {editButton}
                {saveButton}
            </div>
        );

        return component;
    }
};

export default TodoItem;

Similar changes are made to the TodoList component for the openTodo method:

/**
 * Todo list component
 */

import React, { Component } from 'react';

import TodoItem from '../TodoItem';
import { transitionState } from '../../../state/store';

class TodoList extends Component {
    openTodo = (todo) => {
        transitionState('setExpandedTodo', { todo });
    }

    renderTodoListChildren() {
        if (this.props.todoList.length === 0) {
            return (
                <div
                    className="message">
                    No Todos Added Yet.
                </div>
            );
        }

        const todoItems = (this.props.todoList).map(todoItem => {
            console.log(this.props.readOnly);
            return (
                <TodoItem
                    key={todoItem.id}
                    {...todoItem}
                    onClick={this.openTodo.bind(null, todoItem)}
                    readOnly={true}
                    editable={this.props.editable}
                />
            );
        });

        return todoItems;
    }

    render() {
        const todoListChildren = this.renderTodoListChildren();

        const component = (
            <div
                className="todo-list">
                {todoListChildren}
            </div>
        );

        return component;
    }
};

export default TodoList;

The final component to change is of course AppContainer. In addition to changing the transitionState calls, like in the other components, we also register a post transition complete handler in the constructor. This handler basically calls setState on the AppContainer component, using the newly transitioned state, so we can remove all the tedious setState calls that were previously there.

/**
 * App Root Container Component
 */

import React, { Component } from 'react';

import './AppContainer.scss';

import Sidebar from '../Sidebar';
import Content from '../Content';
import { generateID } from '../../utils';
import { transitionState, getState, onStateTransition } from '../../state/store';

class App extends Component {
    constructor(props) {
        super(props);

        this.state = getState();

        onStateTransition((newState) => {
            this.setState(newState);
        });
    }

    addTodo = () => {
        transitionState('addTodo', { id: generateID() });
    }

    saveTodo = () => {
        transitionState('saveExpandedTodo');
    }

    setVisibilityFilter = (visibilityFilter) => {
        transitionState('setVisibilityFilter', { visibilityFilter });
    }

    toggleEditable = () => {
        transitionState('toggleEditable');
    }

    renderExpandedTodo() {
        if (!this.state.expandedTodo)
            return null;

        return (
            <Content
                todo={this.state.expandedTodo}
                editable={this.state.expandedTodoEditable}
                saveTodo={this.saveTodo}
                toggleEditable={this.toggleEditable} />
        );
    }

    render() {
        const expandedTodo = this.renderExpandedTodo();

        const component = (
            <div
                className="app-container">
                <Sidebar
                    todoList={this.state.todoList}
                    addTodo={this.addTodo}
                    setVisibilityFilter={this.setVisibilityFilter}
                    visibilityFilter={this.state.visibilityFilter} />
                {expandedTodo}
            </div>
        );

        return component;
    }
};

export default App;

There, now we have succesfully centralized both state and the state management and transitions.

[Update January 2019] You can checkout the tag 'centralized-state-transitions' on Github, to get the complete code until this point.


Having a central state object and transitions is nice, but isn't passing props getting tedious?

Why yes, yes it is. If you notice, we are now drilling props through pretty much every component. When a component does not itself use a prop, but accepts it, for the sole purpose of passing it along to another component, the component is said to be drilling these props through to the child component.

Even though our example todo application is a pretty small application, it is quite obvious how tedious and repetitive prop drilling becomes even in such a small application. The repetitive nature of prop drilling also introduces scope for bugs being introduced to incorrect prop drilling, especially when drilling across multiple layers of components. So next, let's look at how we can eliminate prop drilling.

To reduce prop drilling we need some way to pass the drilled props directly from the component where it is generated to the component which actually uses it. To do this, we are going to modify our store, and create a new context object. Any component can set or get an item on the context object using the setContextItem and getContextItem helpers. Once we make our changes store.js looks like:

/**
 * Global State Store
 */

let state = {
    todoList: [],
    visibilityFilter: 'all',
    expandedTodo: null,
    expandedTodoEditable: false
};

let context = {};

let transitionCompleteHandler;

function getState() {
    return state;
};

const transforms = {
    taskChange: ({ taskId, fieldName, value }) => {
        fieldName = fieldName === 'checked' ? 'done' : fieldName;

        const expandedTodoTasks = state.expandedTodo.tasks || [];

        let taskIndex = expandedTodoTasks.findIndex(taskItem => taskItem.id === taskId);
        taskIndex = taskIndex === -1 ? expandedTodoTasks.length : taskIndex;

        let task = Object.assign({}, (expandedTodoTasks[taskIndex] || {}), { id: taskId, [fieldName]: value });

        const tasks = [...expandedTodoTasks.slice(0, taskIndex), task, ...expandedTodoTasks.slice(taskIndex + 1)];

        const completedTasks = tasks.filter(task => task.done);
        const done = (completedTasks.length === tasks.length);

        const expandedTodo = Object.assign({}, state.expandedTodo, {
            tasks,
            done
        });

        return { expandedTodo };
    },

    todoChange: ({ fieldName, value }) => {
        fieldName = fieldName === 'checked' ? 'done' : fieldName;

        let tasks = state.expandedTodo.tasks;

        if (fieldName === 'done') {
            tasks = tasks.map(taskItem => {
                return Object.assign({}, taskItem, { done: value });
            });
        }

        const expandedTodo = Object.assign({}, state.expandedTodo, {
            [fieldName]: value,
            tasks
        });

        return { expandedTodo };
    },

    setExpandedTodo: ({ todo }) => {
        return { expandedTodo: todo, expandedTodoEditable: false };
    },

    addTodo: ({ id }) => {
        return {
            expandedTodo: { id },
            expandedTodoEditable: true
        };
    },

    saveExpandedTodo: () => {
        const todo = state.expandedTodo;

        let todoIndex = state.todoList.findIndex(todoItem => todoItem.id === todo.id);
        todoIndex = todoIndex === -1 ? state.todoList.length : todoIndex;

        const todoList = [...state.todoList.slice(0, todoIndex), todo, ...state.todoList.slice(todoIndex + 1)];
        return {
            todoList,
            expandedTodo: todo,
            expandedTodoEditable: false
        };
    },

    setVisibilityFilter: ({ visibilityFilter }) => {
        return { visibilityFilter };
    },

    toggleEditable: () => {
        return { expandedTodoEditable: !state.expandedTodoEditable };
    }
};

function transitionState(name, data) {
    const stateDiff = transforms[name](data);

    state = Object.assign({}, state, stateDiff);

    if (transitionCompleteHandler)
        transitionCompleteHandler(state);

    return state;
};

function onStateTransition(handler) {
    transitionCompleteHandler = handler;
};

function setContextItem(key, value) {
    context[key] = value;
};

function getContextItem(key) {
    return context[key];
};

export { transitionState, getState, onStateTransition, setContextItem, getContextItem };

In our application, pretty much all the drilled props are generated at the AppContainer component. So let's first modify the AppContainer to add the drilled props to the context.

/**
 * App Root Container Component
 */

import React, { Component } from 'react';

import './AppContainer.scss';

import Sidebar from '../Sidebar';
import Content from '../Content';
import { generateID } from '../../utils';
import { transitionState, getState, onStateTransition, setContextItem } from '../../state/store';

class App extends Component {
    constructor(props) {
        super(props);

        this.state = getState();

        onStateTransition((newState) => {
            this.updateStateContextItems(newState);
            this.setState(newState);
        });

        setContextItem('addTodo', this.addTodo);
        setContextItem('saveTodo', this.saveTodo);
        setContextItem('toggleEditable', this.toggleEditable);
        setContextItem('setVisibilityFilter', this.setVisibilityFilter);
        this.updateStateContextItems();
    }

    updateStateContextItems(state = this.state) {
        setContextItem('visibilityFilter', state.visibilityFilter);
        setContextItem('expandedTodoEditable', state.expandedTodoEditable);
    }

    addTodo = () => {
        transitionState('addTodo', { id: generateID() });
    }

    saveTodo = () => {
        transitionState('saveExpandedTodo');
    }

    setVisibilityFilter = (visibilityFilter) => {
        transitionState('setVisibilityFilter', { visibilityFilter });
    }

    toggleEditable = () => {
        transitionState('toggleEditable');
    }

    renderExpandedTodo() {
        if (!this.state.expandedTodo)
            return null;

        return (
            <Content
                todo={this.state.expandedTodo} />
        );
    }

    render() {
        const expandedTodo = this.renderExpandedTodo();

        const component = (
            <div
                className="app-container">
                <Sidebar
                    todoList={this.state.todoList} />
                {expandedTodo}
            </div>
        );

        return component;
    }
};

export default App;

As you can see we are no longer passing the addTodo, setVisibilityFilter, or visibilityFilter props to SidebarContainer. We are also not passing saveTodo, toggleEditable, or editable props to Content component. These props are instead added to the context object using the setContextItem method. We have also modified the post state transition handler, to update the context items, after any state transitions. Since these props are no longer passed to SidebarContainer and Content components, we can modify these to remove prop drilling.

Now, the Sidebar component no longer get's the addTodo, setVisibilityFilter, or visibilityFilter props from it's props. Instead let's modify Sidebar component to get these from the context.

/**
 * Sidebar component
 */

import React, { Component } from 'react';

import './Sidebar.scss';
import TodoList from '../_shared/TodoList';
import { getContextItem } from '../../state/store';

const headerPrefixByVisibilityFilter = {
    all: 'All',
    none: 'Not Started',
    partial: 'Partly Completed',
    done: 'Completed'
};

class Sidebar extends Component {
    renderHeader() {
        const component = (
            <div
                className="header">
                <span>{headerPrefixByVisibilityFilter[getContextItem('visibilityFilter')]} Todos </span>
                <button
                    onClick={getContextItem('addTodo')}>
                    Add Todo
                </button>
            </div>
        );

        return component;
    }

    renderFooter() {
        const setVisibilityFilter = getContextItem('setVisibilityFilter');

        const component = (
            <div
                className="footer">
                <span>Show todos: </span>
                <button
                    onClick={setVisibilityFilter.bind(null, 'all')} >
                    All
                </button>
                <button
                    onClick={setVisibilityFilter.bind(null, 'none')} >
                    Not Started
                </button>
                <button
                    onClick={setVisibilityFilter.bind(null, 'partial')} >
                    Partly Done
                </button>
                <button
                    onClick={setVisibilityFilter.bind(null, 'done')} >
                    Completed
                </button>
            </div>
        );

        return component;
    }

    renderTodoList() {
        let todoListItems;
        const visibilityFilter = getContextItem('visibilityFilter');

        if (visibilityFilter === 'all')
            todoListItems = this.props.todoList;
        else if (visibilityFilter === 'none')
            todoListItems = this.props.notStartedTodoList;
        else if (visibilityFilter === 'partial')
            todoListItems = this.props.partlyCompletedTodoList;
        else if (visibilityFilter === 'done')
            todoListItems = this.props.completedTodoList;

        const component = (
            <TodoList
                todoList={todoListItems} />
        );

        return component;
    }

    render() {
        const header = this.renderHeader();
        const footer = this.renderFooter();
        const todoList = this.renderTodoList();

        const component = (
            <div
                className="sidebar">
                {header}
                {todoList}
                {footer}
            </div>
        );

        return component;
    }
};

export default Sidebar;

As you can see, we are getting the props from the store's context using the getContextItem method, rather than getting them from the props.

Making similar changes to TodoItem component:

/**
 * Todo Item Component
 */

import React, { Component } from 'react';

import CheckableItem from '../CheckableItem';
import { generateID } from '../../../utils';
import { transitionState, getContextItem } from '../../../state/store';

class TodoItem extends Component {
    static get defaultProps() {
        return {
            tasks: []
        };
    }

    onTaskChange = (taskId, fieldName, value) => {
        transitionState('taskChange', { taskId, fieldName, value });
    }

    onTodoChange = (fieldName, value) => {
        transitionState('todoChange', { fieldName, value });
    }

    renderTaskItems() {
        if (!this.props.showTasks)
            return;

        const editable = getContextItem('expandedTodoEditable');

        const tasks = editable ? this.props.tasks.concat([{
            id: generateID()
        }]) : this.props.tasks;

        const taskItems = tasks.map(taskItem => {
            return (
                <CheckableItem
                    key={taskItem.id}
                    onChange={this.onTaskChange.bind(null, taskItem.id)}
                    className="task-item"
                    checked={taskItem.done}
                    checkable={!editable}
                    description={taskItem.description}
                    readOnly={taskItem.readOnly || !editable} />
            );
        });

        return (
            <div
                className="task-list">
                <div
                    className="caption">
                    Tasks
                </div>
                {taskItems}
            </div>
        );
    }

    renderSaveButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={getContextItem('saveTodo')}>
                Save
            </button>
        );
    }

    renderEditButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={getContextItem('toggleEditable')}>
                {getContextItem('expandedTodoEditable') ? 'Reset' : 'Edit'}
            </button>
        );
    }

    render() {
        const taskItems = this.renderTaskItems();
        const saveButton = this.renderSaveButton();
        const editButton = this.renderEditButton();

        const component = (
            <div
                className="todo-item">
                <CheckableItem
                    className="header"
                    onChange={this.onTodoChange}
                    onClick={this.props.onClick}
                    checked={this.props.done}
                    checkable={!this.props.readOnly}
                    description={this.props.description}
                    readOnly={this.props.readOnly&nbsp;|| !getContextItem('expandedTodoEditable')} />
                {taskItems}
                {editButton}
                {saveButton}
            </div>
        );

        return component;
    }
};

export default TodoItem;

In the TodoItem component, we have replaced getting saveTodo, toggleEditable, and editable props from the props with getting them from the store's context using the getContextItem API.

And that's pretty much it. We've succesfully reduced prop drilling by using a pluggable context object instead.

[Update January 2019] You can checkout the tag 'pluggable-context' on Github, to get the complete code until this point.


So what's all this got to do with Redux?

By now you might be wondering why we've pretty much completely ignored talking about Redux for the vast majority of the article. While I may not have mentioned Redux by name, the concept of having a centralized store, managing state transitions in the store, triggering state transitions using pure transform functions by dispatching a name and data, and reducing prop-drilling by plugging in your store to each component individually, are the basic core concepts of Redux.

Before we can go about replacing our custom store with Redux, we first need to understand a few Redux concepts. These are things we have already come across in this article, and Redux just uses some different terminology. So let's familiarize ourselves with Redux terminology.

  • Store - A redux store is created using the createStore method exposed by Redux. This store provided by Redux is basically the same thing as our current store. The Store exposes the following methods:

    • getState - This method does the exact same thing that our custom store's getState method does – it returns the current state as tracked by the store.
    • subscribe - Similar to how we were registering a post transition event handler using the onStateTransition method of our custom store, subscribe allows you to register a function to be called when a state transition occurs. The difference here is that when your subscribed function is called, the newly transitioned state is not provided as a parameter, and you have to get it by using getState.
    • dispatch - This is similar to our custom store's transitionState method. It takes a single argument, which in Redux terminology is called an action. Every Redux action has a type property, which is the exact same as the name parameter we were passing to the transitionState method. In addition to the mandatory type property, you can also provide any other data that might be required. So we replace all our transitionState(name, data) calls with store.dispatch({type: name, ...data}) calls.
  • Reducer - A Redux reducer is similar to the transform functions we have in our custom store. A reducer accepts as it's parameters, the current state and the action that was dispatched. Based on the type of the action, and the data provided by the action, and the provided state, the reducer generates a new state and returns that state. In Redux the state is always treated as immutable.

  • combineReducers - Instead of having a single large reducer which handles the entire state of your application, you can split your reducers into multiple different functions. Each reducer will then be responsible for generating the state of only a single property on the state object.

However, to create a store using createStore you can only provide a single reducer. So we'll need to combine our multiple reducers into a single reducer. This is done using the combineReducers method exposed by Redux. combineReducers accepts an object defining the relationship between state properties and the corresopnding reducer responsible for handling it, and merges them all into a single reducer.

In addition, when any individual reducer that was combined is triggered, it will only be provided with the part of the state that it is responsible for managing as specified in combineReducers. Similarly the state that it returns will only be the subset of the state that it is responsible for managing. combineReducers will automatically merge this into the complete state object.

So now we have a basic idea of Redux terminology, and how it maps to our custom store, let's replace our custom store with Redux. The majority of the changes go in store.js:

/**
 * Global State Store
 */

import { createStore, combineReducers } from 'redux';

let context = {};

function todoList(state = [], action) {
    if (action.type !== 'saveExpandedTodo')
        return state;

    const { expandedTodo } = action;

    let todoIndex = state.findIndex(todoItem => todoItem.id === expandedTodo.id);
    todoIndex = todoIndex === -1 ? state.length : todoIndex;

    const todoList = [...state.slice(0, todoIndex), expandedTodo, ...state.slice(todoIndex + 1)];
    return todoList;
};

function visibilityFilter(state = 'all', action) {
    if (action.type !== 'setVisibilityFilter')
        return state;

    return action.visibilityFilter || 'all';
};

function taskChange(expandedTodo, { taskId, fieldName, value }) {
    fieldName = fieldName === 'checked' ? 'done' : fieldName;

    const expandedTodoTasks = expandedTodo.tasks || [];

    let taskIndex = expandedTodoTasks.findIndex(taskItem => taskItem.id === taskId);
    taskIndex = taskIndex === -1 ? expandedTodoTasks.length : taskIndex;

    let task = Object.assign({}, (expandedTodoTasks[taskIndex] || {}), { id: taskId, [fieldName]: value });

    const tasks = [...expandedTodoTasks.slice(0, taskIndex), task, ...expandedTodoTasks.slice(taskIndex + 1)];

    const completedTasks = tasks.filter(task => task.done);
    const done = (completedTasks.length === tasks.length);

    return Object.assign({}, expandedTodo, {
        tasks,
        done
    });
};

function todoChange(expandedTodo, { fieldName, value }) {
    fieldName = fieldName === 'checked' ? 'done' : fieldName;

    let tasks = expandedTodo.tasks;

    if (fieldName === 'done') {
        tasks = tasks.map(taskItem => {
            return Object.assign({}, taskItem, { done: value });
        });
    }

    return Object.assign({}, expandedTodo, {
        [fieldName]: value,
        tasks
    });
};

function setExpandedTodo({ todo }) {
    return todo;
};

function addTodo({ id }) {
    return { id };
};

function expandedTodo(state = null, action) {
    if (action.type === 'taskChange')
        return taskChange(state, action);

    if (action.type === 'todoChange')
        return todoChange(state, action);

    if (action.type === 'setExpandedTodo')
        return setExpandedTodo(action);

    if (action.type === 'addTodo')
        return addTodo(action);

    return state;
};

function expandedTodoEditable(state = false, action) {
    if (action.type === 'setExpandedTodo')
        return false;

    if (action.type === 'addTodo')
        return true;

    if (action.type === 'saveExpandedTodo')
        return false;

    if (action.type === 'toggleEditable')
        return !state;

    return state;
};

const reducers = {
    todoList,
    visibilityFilter,
    expandedTodo,
    expandedTodoEditable
};

const reducer = combineReducers(reducers);
const store = createStore(reducer);

function setContextItem(key, value) {
    context[key] = value;
};

function getContextItem(key) {
    return context[key];
};

export { store, setContextItem, getContextItem };

As you can see we've gotten rid of transitionState, onStateTransition, getState and state. The methods that used to be defined in the transforms object have now been modified to handle only the subset of the state that they are responsible for.

One major difference that you may notice is that previously a few methods were changing multiple properties on state, which is no longer the case. For example, the addTodo method previously modified the state to set both expandedTodo and expandedTodoEditable properties. However, currently it only set's the expandedTodo property.

The reason for this is that we are using multiple reducers and combining them using combineReducers. When we are combining the reducers we specify that the expandedTodo reducer only handles the subset of state referred by the expandedTodo state property. So how do we set the expandedTodoEditable property when a new todo is added?

Well, we can do this by listening of the addTodo action in both the expandedTodo and expandedTodoEditable reducers. this works because, When we combine reducers by using combineReducers, any action that is dispatched are sent to every single reducer, so each reducer can respond independantly to that action.

These reducers are combined to form a single reducer using combineReducers, and that reducer is passed along to createStore to create a new Redux store, which we then export.

Now let's go ahead, and change the AppContainer to use the new Redux store:

/**
 * App Root Container Component
 */

import React, { Component } from 'react';

import './AppContainer.scss';

import Sidebar from '../Sidebar';
import Content from '../Content';
import { generateID } from '../../utils';
import { store, setContextItem } from '../../state/store';

class App extends Component {
    constructor(props) {
        super(props);

        this.state = store.getState();

        store.subscribe(() => {
            const newState = store.getState();

            this.updateStateContextItems(newState);
            this.setState(newState);
        });

        setContextItem('addTodo', this.addTodo);
        setContextItem('saveTodo', this.saveTodo);
        setContextItem('toggleEditable', this.toggleEditable);
        setContextItem('setVisibilityFilter', this.setVisibilityFilter);
        this.updateStateContextItems();
    }

    updateStateContextItems(state = this.state) {
        setContextItem('visibilityFilter', state.visibilityFilter);
        setContextItem('expandedTodoEditable', state.expandedTodoEditable);
    }

    addTodo = () => {
        store.dispatch({ type: 'addTodo', id: generateID() });
    }

    saveTodo = () => {
        store.dispatch({ type: 'saveExpandedTodo', expandedTodo: this.state.expandedTodo });
    }

    setVisibilityFilter = (visibilityFilter) => {
        store.dispatch({ type: 'setVisibilityFilter', visibilityFilter });
    }

    toggleEditable = () => {
        store.dispatch({ type: 'toggleEditable' });
    }

    renderExpandedTodo() {
        if (!this.state.expandedTodo)
            return null;

        return (
            <Content
                todo={this.state.expandedTodo} />
        );
    }

    render() {
        const expandedTodo = this.renderExpandedTodo();

        const component = (
            <div
                className="app-container">
                <Sidebar
                    todoList={this.state.todoList} />
                {expandedTodo}
            </div>
        );

        return component;
    }
};

export default App;

Since the Redux store is so similar to the custom store that we were using, the changes in the AppContainer component, are pretty much just cosmetic. We replace transitionState(name, data) with store.dispatch({type: name, ...data}), onStateTransition with store.subscribe and getState with store.getState.

Making similar changes to TodoList:

/**
 * Todo list component
 */

import React, { Component } from 'react';

import TodoItem from '../TodoItem';
import { store } from '../../../state/store';

class TodoList extends Component {
    openTodo = (todo) => {
        store.dispatch({ type: 'setExpandedTodo', todo });
    }

    renderTodoListChildren() {
        if (this.props.todoList.length === 0) {
            return (
                <div
                    className="message">
                    No Todos Added Yet.
                </div>
            );
        }

        const todoItems = (this.props.todoList).map(todoItem => {
            console.log(this.props.readOnly);
            return (
                <TodoItem
                    key={todoItem.id}
                    {...todoItem}
                    onClick={this.openTodo.bind(null, todoItem)}
                    readOnly={true}
                />
            );
        });

        return todoItems;
    }

    render() {
        const todoListChildren = this.renderTodoListChildren();

        const component = (
            <div
                className="todo-list">
                {todoListChildren}
            </div>
        );

        return component;
    }
};

export default TodoList;

Again, the changes are cosmetic, and all we are doing is replacing transitionState with store.dispatch.

Similar cosmetic changes are required for TodoItem component as well:

/**
 * Todo Item Component
 */

import React, { Component } from 'react';

import CheckableItem from '../CheckableItem';
import { generateID } from '../../../utils';
import { store, getContextItem } from '../../../state/store';

class TodoItem extends Component {
    static get defaultProps() {
        return {
            tasks: []
        };
    }

    onTaskChange = (taskId, fieldName, value) => {
        store.dispatch({ type: 'taskChange', taskId, fieldName, value });
    }

    onTodoChange = (fieldName, value) => {
        store.dispatch({ type: 'todoChange', fieldName, value });
    }

    renderTaskItems() {
        if (!this.props.showTasks)
            return;

        const editable = getContextItem('expandedTodoEditable');

        const tasks = editable ? this.props.tasks.concat([{
            id: generateID()
        }]) : this.props.tasks;

        const taskItems = tasks.map(taskItem => {
            return (
                <CheckableItem
                    key={taskItem.id}
                    onChange={this.onTaskChange.bind(null, taskItem.id)}
                    className="task-item"
                    checked={taskItem.done}
                    checkable={!editable}
                    description={taskItem.description}
                    readOnly={taskItem.readOnly || !editable} />
            );
        });

        return (
            <div
                className="task-list">
                <div
                    className="caption">
                    Tasks
                </div>
                {taskItems}
            </div>
        );
    }

    renderSaveButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={getContextItem('saveTodo')}>
                Save
            </button>
        );
    }

    renderEditButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={getContextItem('toggleEditable')}>
                {getContextItem('expandedTodoEditable') ? 'Reset' : 'Edit'}
            </button>
        );
    }

    render() {
        const taskItems = this.renderTaskItems();
        const saveButton = this.renderSaveButton();
        const editButton = this.renderEditButton();

        const component = (
            <div
                className="todo-item">
                <CheckableItem
                    className="header"
                    onChange={this.onTodoChange}
                    onClick={this.props.onClick}
                    checked={this.props.done}
                    checkable={!this.props.readOnly}
                    description={this.props.description}
                    readOnly={this.props.readOnly&nbsp;|| !getContextItem('expandedTodoEditable')} />
                {taskItems}
                {editButton}
                {saveButton}
            </div>
        );

        return component;
    }
};

export default TodoItem;

And as simple as that, we've replaced our custom store with a Redux store.

[Update January 2019] You can checkout the tag 'redux-store' on Github, to get the complete code until this point.


Wait a minute, we're still using the custom context, aren't we?

Yes we are indeed still using the custom context object and it's associated getContextItem and setContextItem methods. Redux doesn't provide this functionality out of the box. This is because Redux is not exactly meant to be used only with React. It's a generic library, which just happens to be most popularly used with React.

However, we can use the React-Redux library which basically acts as a bridge between Redux and React, and provides this functionality for us. But React-Redux doesn't like actions much, and it prefers to deal with actionCreators instead.

In Redux, an actionCreator is basically just a function, which accepts some data as a parameter, uses that data to construct an action object, and returns that action object. Basically actionCreators make it easy to repeatedly generate the same action. So before we can use React-Redux, we first need to create actionCreators to create actions for us, instead of manually creating actions.

So let's change store.js to get rid of context, setContextItem and getContextItem, and replace them with actionCreators:

/**
 * Global State Store
 */

import { createStore, combineReducers } from 'redux';

function todoList(state = [], action) {
    if (action.type !== 'saveExpandedTodo')
        return state;

    const { expandedTodo } = action;

    let todoIndex = state.findIndex(todoItem => todoItem.id === expandedTodo.id);
    todoIndex = todoIndex === -1 ? state.length : todoIndex;

    const todoList = [...state.slice(0, todoIndex), expandedTodo, ...state.slice(todoIndex + 1)];
    return todoList;
};

function visibilityFilter(state = 'all', action) {
    if (action.type !== 'setVisibilityFilter')
        return state;

    return action.visibilityFilter || 'all';
};

function taskChange(expandedTodo, { taskId, fieldName, value }) {
    fieldName = fieldName === 'checked' ? 'done' : fieldName;

    const expandedTodoTasks = expandedTodo.tasks || [];

    let taskIndex = expandedTodoTasks.findIndex(taskItem => taskItem.id === taskId);
    taskIndex = taskIndex === -1 ? expandedTodoTasks.length : taskIndex;

    let task = Object.assign({}, (expandedTodoTasks[taskIndex] || {}), { id: taskId, [fieldName]: value });

    const tasks = [...expandedTodoTasks.slice(0, taskIndex), task, ...expandedTodoTasks.slice(taskIndex + 1)];

    const completedTasks = tasks.filter(task => task.done);
    const done = (completedTasks.length === tasks.length);

    return Object.assign({}, expandedTodo, {
        tasks,
        done
    });
};

function todoChange(expandedTodo, { fieldName, value }) {
    fieldName = fieldName === 'checked' ? 'done' : fieldName;

    let tasks = expandedTodo.tasks;

    if (fieldName === 'done') {
        tasks = tasks.map(taskItem => {
            return Object.assign({}, taskItem, { done: value });
        });
    }

    return Object.assign({}, expandedTodo, {
        [fieldName]: value,
        tasks
    });
};

function setExpandedTodo({ todo }) {
    return todo;
};

function addTodo({ id }) {
    return { id };
};

function expandedTodo(state = null, action) {
    if (action.type === 'taskChange')
        return taskChange(state, action);

    if (action.type === 'todoChange')
        return todoChange(state, action);

    if (action.type === 'setExpandedTodo')
        return setExpandedTodo(action);

    if (action.type === 'addTodo')
        return addTodo(action);

    return state;
};

function expandedTodoEditable(state = false, action) {
    if (action.type === 'setExpandedTodo')
        return false;

    if (action.type === 'addTodo')
        return true;

    if (action.type === 'saveExpandedTodo')
        return false;

    if (action.type === 'toggleEditable')
        return !state;

    return state;
};

const reducers = {
    todoList,
    visibilityFilter,
    expandedTodo,
    expandedTodoEditable
};

const reducer = combineReducers(reducers);
const store = createStore(reducer);

const actionCreators = {
    addTodo: (id) => ({ type: 'addTodo', id }),

    saveExpandedTodo: (expandedTodo) => ({ type: 'saveExpandedTodo', expandedTodo }),

    setVisibilityFilter: (visibilityFilter) => ({ type: 'setVisibilityFilter', visibilityFilter }),

    toggleEditable: () => ({ type: 'toggleEditable' }),

    setExpandedTodo: (todo) => ({ type: 'setExpandedTodo', todo }),

    todoChange: (fieldName, value) => ({ type: 'todoChange', fieldName, value }),

    taskChange: (taskId, fieldName, value) => ({ type: 'taskChange', taskId, fieldName, value })
};

export { store, actionCreators };

Now that we've gotten our actionCreators setup, let's replace getContextItem and setContextItem in the other components. To do this, we wrap our entire app with React-Redux's Provider. The Provider is used to make our Redux store accessible to all the child component's of the Provider.

So change index.js to:

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';

import App from './view/App';
import { store } from './state/store';

render(
    (
        <Provider
            store={store} >
            <App />
        </Provider>
    ),
    document.getElementById('root')
);

Now that the store has been made available to all the child components, we still need to wire up the state and actions that each component uses. React-Redux exposes a method called connect which helps us wire up the state and action creators to the components props.

The connect method takes two input parameters

  • mapStateToProps - This is a function which is called once, when the component is instantiated. This method is called with the state of the entire application. This method should then extract the state properties that are required by that component, and return them as an object. React-Redux then makes each property of that object available to the component as a prop item. Whenever any of these properties change, React-Redux will automatically trigger a re-render, with the updated props.
  • mapDispatchToProps - This is an object, which maps property names to action creators. connect converts each action creator into a function, which automatically dispatches the action to the store, whenever called.

The connect method outputs a function to which you pass the component that should be wired. So let's wire up our components.

Let's first look at the TodoItem component:

/**
 * Todo Item Component
 */

import React, { Component } from 'react';
import { connect } from 'react-redux';

import CheckableItem from '../CheckableItem';
import { generateID } from '../../../utils';
import { actionCreators } from '../../../state/store';

class TodoItem extends Component {
    static get defaultProps() {
        return {
            tasks: []
        };
    }

    onTaskChange = (taskId, fieldName, value) => {
        this.props.taskChange(taskId, fieldName, value);
    }

    onTodoChange = (fieldName, value) => {
        this.props.todoChange(fieldName, value);
    }

    saveTodo = () => {
        const { id, description, tasks, done } = this.props;
        const todo = { id, description, tasks, done };

        this.props.saveExpandedTodo(todo);
    }

    renderTaskItems() {
        if (!this.props.showTasks)
            return;

        const editable = this.props.expandedTodoEditable;

        const tasks = editable ? this.props.tasks.concat([{
            id: generateID()
        }]) : this.props.tasks;

        const taskItems = tasks.map(taskItem => {
            return (
                <CheckableItem
                    key={taskItem.id}
                    onChange={this.onTaskChange.bind(null, taskItem.id)}
                    className="task-item"
                    checked={taskItem.done}
                    checkable={!editable}
                    description={taskItem.description}
                    readOnly={taskItem.readOnly || !editable} />
            );
        });

        return (
            <div
                className="task-list">
                <div
                    className="caption">
                    Tasks
                </div>
                {taskItems}
            </div>
        );
    }

    renderSaveButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={this.saveTodo}>
                Save
            </button>
        );
    }

    renderEditButton() {
        if (this.props.readOnly)
            return;

        return (
            <button
                onClick={this.props.toggleEditable}>
                {this.props.expandedTodoEditable ? 'Reset' : 'Edit'}
            </button>
        );
    }

    render() {
        const taskItems = this.renderTaskItems();
        const saveButton = this.renderSaveButton();
        const editButton = this.renderEditButton();

        const component = (
            <div
                className="todo-item">
                <CheckableItem
                    className="header"
                    onChange={this.onTodoChange}
                    onClick={this.props.onClick}
                    checked={this.props.done}
                    checkable={!this.props.readOnly}
                    description={this.props.description}
                    readOnly={this.props.readOnly&nbsp;|| !this.props.expandedTodoEditable} />
                {taskItems}
                {editButton}
                {saveButton}
            </div>
        );

        return component;
    }
};

const { taskChange, todoChange, saveExpandedTodo, toggleEditable } = actionCreators;
const mapDispatchToProps = { taskChange, todoChange, saveExpandedTodo, toggleEditable };

const mapStateToProps = (state) => ({ expandedTodoEditable: state.expandedTodoEditable });

export default connect(mapStateToProps, mapDispatchToProps)(TodoItem);

The major changes are the mapStateToProps and mapDispatchToProps that we pass to React-Redux's connect method. We then pass in the TodoItem and the resulting wired component is what we export. Now that we have wired up the component, the actionCreators are now available as props on the TodoItem. So we can replace every store.dispatch(action) call with the corresponding prop item call. Similarly we can also replace all the getContextItem calls with corresponding prop items.

Making similar changes to TodoList component:

/**
 * Todo list component
 */

import React, { Component } from 'react';
import { connect } from 'react-redux';

import TodoItem from '../TodoItem';
import { actionCreators } from '../../../state/store';

class TodoList extends Component {
    openTodo = (todo) => {
        this.props.setExpandedTodo(todo);
    }

    renderTodoListChildren() {
        if (this.props.todoList.length === 0) {
            return (
                <div
                    className="message">
                    No Todos Added Yet.
                </div>
            );
        }

        const todoItems = (this.props.todoList).map(todoItem => {
            console.log(this.props.readOnly);
            return (
                <TodoItem
                    key={todoItem.id}
                    {...todoItem}
                    onClick={this.openTodo.bind(null, todoItem)}
                    readOnly={true}
                />
            );
        });

        return todoItems;
    }

    render() {
        const todoListChildren = this.renderTodoListChildren();

        const component = (
            <div
                className="todo-list">
                {todoListChildren}
            </div>
        );

        return component;
    }
};

const { setExpandedTodo } = actionCreators;
const mapDispatchToProps = { setExpandedTodo };

export default connect(null, mapDispatchToProps)(TodoList);

TodoList component does not actually require any state property, so we do not need to map state to props. So the mapStateToProps does not exist. However, we still need to dispatch a few actions, so we do have mapDispatchToProps and uses connect to wire up the component.

Making similar changes to Sidebar component:

/**
 * Sidebar component
 */

import React, { Component } from 'react';
import { connect } from 'react-redux';

import './Sidebar.scss';
import TodoList from '../_shared/TodoList';
import { generateID } from '../../utils';
import { actionCreators } from '../../state/store';

const headerPrefixByVisibilityFilter = {
    all: 'All',
    none: 'Not Started',
    partial: 'Partly Completed',
    done: 'Completed'
};

class Sidebar extends Component {
    addTodo = () => {
        this.props.addTodo(generateID());
    }

    renderHeader() {
        const component = (
            <div
                className="header">
                <span>{headerPrefixByVisibilityFilter[this.props.visibilityFilter]} Todos </span>
                <button
                    onClick={this.addTodo}>
                    Add Todo
                </button>
            </div>
        );

        return component;
    }

    renderFooter() {
        const setVisibilityFilter = this.props.setVisibilityFilter;

        const component = (
            <div
                className="footer">
                <span>Show todos: </span>
                <button
                    onClick={setVisibilityFilter.bind(null, 'all')} >
                    All
                </button>
                <button
                    onClick={setVisibilityFilter.bind(null, 'none')} >
                    Not Started
                </button>
                <button
                    onClick={setVisibilityFilter.bind(null, 'partial')} >
                    Partly Done
                </button>
                <button
                    onClick={setVisibilityFilter.bind(null, 'done')} >
                    Completed
                </button>
            </div>
        );

        return component;
    }

    renderTodoList() {
        let todoListItems;
        const visibilityFilter = this.props.visibilityFilter;

        if (visibilityFilter === 'all')
            todoListItems = this.props.todoList;
        else if (visibilityFilter === 'none')
            todoListItems = this.props.notStartedTodoList;
        else if (visibilityFilter === 'partial')
            todoListItems = this.props.partlyCompletedTodoList;
        else if (visibilityFilter === 'done')
            todoListItems = this.props.completedTodoList;

        const component = (
            <TodoList
                todoList={todoListItems} />
        );

        return component;
    }

    render() {
        const header = this.renderHeader();
        const footer = this.renderFooter();
        const todoList = this.renderTodoList();

        const component = (
            <div
                className="sidebar">
                {header}
                {todoList}
                {footer}
            </div>
        );

        return component;
    }
};

const { addTodo, setVisibilityFilter } = actionCreators;
const mapDispatchToProps = { addTodo, setVisibilityFilter };

const mapStateToProps = (state) => ({ visibilityFilter: state.visibilityFilter });

export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);

We are replacing getContextItem and store.dispatch with corresponding prop item calls. Previously, addTodo was a prop accessed via getContextItem, which was set in the context in the AppContainer component. However, now we can directly dispatch the action via the corresponding actionCreator through the wired prop item.

Finally we need to wire up the AppContainer component:

/**
 * App Root Container Component
 */

import React, { Component } from 'react';
import { connect } from 'react-redux';

import './AppContainer.scss';

import Sidebar from '../Sidebar';
import Content from '../Content';
import { actionCreators } from '../../state/store';

class App extends Component {
    setVisibilityFilter = (visibilityFilter) => {
        this.props.setVisibilityFilter(visibilityFilter);
    }

    toggleEditable = () => {
        this.props.toggleEditable();
    }

    renderExpandedTodo() {
        if (!this.props.expandedTodo)
            return null;

        return (
            <Content
                todo={this.props.expandedTodo} />
        );
    }

    render() {
        const expandedTodo = this.renderExpandedTodo();

        const component = (
            <div
                className="app-container">
                <Sidebar
                    todoList={this.props.todoList} />
                {expandedTodo}
            </div>
        );

        return component;
    }
};

const { setVisibilityFilter, toggleEditable } = actionCreators;
const mapDispatchToProps = { setVisibilityFilter, toggleEditable };

const mapStateToProps = (state) => {
    const { expandedTodo, todoList } = state;
    return { expandedTodo, todoList };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

Here too we have replaced the setContextItem and store.dispatch calls with the corresponding prop item calls. With that we have replaced our custom context object with React-Redux's Provider. Using React-Redux is a bit verbose, but the verbosity allows us to not have to worry about manually triggering state changes whenever any action is dispatched. In addition React-Redux also takes care of triggering re-renders, whenever a state property used by the wired component is changed.

[Update January 2019] You can checkout the tag 'react-redux' on Github, to get the complete code until this point.

And with that, we have sucessfully integrated Redux and React-Redux into our application!


If our custom store is so similar to a Redux store, why use Redux at all?

Redux provides a lot of helper methods like combineReducers, which essentially makes it a lot easier to write our store code. In our custom store, each transform function was dealing with the entire state of the app, and had to navigate through that complete state to extract the specific data it required. Similarly it had to merge the differential state with the entire pre-existing state. For a small application like our Todo example, this is easy, but as your application's complexity increases, this becomes more and more difficult.

Additionally, Redux also allows you to use Redux middleware, which are quite similar to middleware method in ExpressJS. A Redux middleware sit's between the action dispatcher and the reducer, and allows you to read or modify the action on the fly. This makes things like logging the state of the application before any action occurs possible.

Using Redux can also make debugging your code easier. Redux provides dev tools which allow you to track the changes to state on a per action basis. These dev tools also allow you to undo and redo actions, essentially allowing you to travel back and forth in the timeline of your actions. This is called time-travel debugging and it is quite helpful in speeding up your application development.

Redux also has a huge community of users, who have pretty much thought of every possible scenario where Redux might break, and either fixed it, or come up with a workaround. This huge community is also responsible for producing a lot of Redux middleware libraries. Basically, if you think you have a need for a middleware which performs a specific, not very uncommon action, then chances are there already exists a library which you can use. This reduces the amount of code that you have to write from scratch, saving you time and letting you ship your application faster.

That is not to say that you always have to use Redux. It is completely possible to write React applications without using Redux. Redux does come with a learning curve, and that learning curve only get's steeper and steeper when you add in all the associated libraries that you might want to use in addition to React. However, for most medium to large applications, using Redux will generally end up making your life easier.


I'd love to hear your opinion on things I've written in this article. Unfortunately, I've not yet found a nice lightweight comments widget to add on to my blog, so until then the only way I can take comments is via email. Do write to me at asleepysamurai at google's mail service.