Step By Step Guide To Todolist App With React And Redux Using create-react-app

Spread the love

In this article, I am going to help you to learn the fundamentals of react and redux. We will do this by implementing a todolist app using react and redux.

Here I am mainly focusing on react basics and redux concepts. So I am not going into the configuration and setup sections. For this, we will use create-react-app for boilerplate.

To check the full working code of this demo, Click here.

So let’s start.

React Redux Todolist

We will start this tutorial by creating a project with create-react-app.

Navigate to your folder and run the following command.

npx create-react-app react-redux-todolist

When you run this command, you may be getting this error.

A template was not provided. This is likely because you're using an outdated version of create-react-app. 
Please note that global installs of create-react-app are no longer supported.

The primary solution will be uninstalling create-react-app globally with the following command,

npm uninstall -g create-react-app

After this, try recreating a project folder with npx command above. If you still get the above error, run the following command

rm -rf /usr/local/bin/create-react-app

Now, recreate the folder again, It will work fine now.

Finally we have created the project folder with the name react-redux-todolist. Now run the project using the npm or yarn as your wish.

yarn start

You can see the default react template in browser on url http://localhost:3000/

Our project is ready for development now. We are going to implement the Todolist app using redux. So before getting into the development, let’s discuss the redux and its core concepts.

Redux

From the documentation, Redux is basically a predictable state container for javascript apps.

In simple words, redux is one of the design patterns of javascript for your project. You will have a global object which contains all the data of your project. You perform CRUD operations on that object based on your requirements. You use this object for every event/operation in your project.

That’s it. This is all about Redux. I am going to elaborate the above paragraph in redux terminology.

Core Concepts Of Redux

I won’t go deep into the concepts but I will give an overview on it. Redux documentation is pretty good to understand the concepts theoretically.

Basically 3 core concepts in redux.

  1. Store
  2. Actions
  3. Reducers

What is Store ?

The store is an object which holds all the application data. This is the global object we are talking about. This holds the state of our application and this can be used everywhere in our project.

Actions

Actions are plain javascript objects. This is the object that sends data from your application to your store. It has one mandatory property, type which indicates what kind of operation(CRUD) it needs to perform. It has another property called payload(you can change the name) which holds the data to pass to the store.

Reducers

Reducers are simple javascript functions. It takes two parameters. One is the action which has type and payload and other is the current state of the application.

Note that, this function won’t mutate the state. It takes the copy of the current state, updates the state based on the action and returns a new state.

An application can have more than one reducer functions. We will combine them together using a combineReducers() function from redux.

Now, Let’s start working on our demo.

First, we need to install dependencies required for using the redux. We need to install to libraries ‘redux’ and ‘react-redux’. We need react-redux to make redux work in react project.

npm install --save redux react-redux

Unlike the example in the documentation, I’m going to make few changes in the folder structure for clear understanding. I will keep the App.js at root level.

We need 3 top level components to implement this.

  1. Add Todo – input field to add tasks
  2. Todo List – list of added tasks
  3. Filters – To show task lists based on filters like active and completed.

Let’s create static components initially with static data to visualize how does our actual demo look like.

We are not having containers and components separately. We will only create components for the above inside components folder. The code right now look like this.

App.js

import React from 'react';
import './App.css';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
import Filters from './components/Filters';

function App() {
  return (
    <div className="App">
      <AddTodo />
      <TodoList />
      <Filters />
    </div>
  );
}

export default App;

components/AddTodo.js

import React from 'react';

const AddTodo = () => {

    return (
        <div>Add Todo</div>
    )
}

export default AddTodo;

components/TodoList.js

import React from 'react';

const TodoList = () => {

    return (
        <div>TodoList</div>
    )
}

export default TodoList;

components/Filters.js

import React from 'react';

const Filters = () => {

    return (
        <div>Filters</div>
    )
}

export default Filters;

Let’s implement the AddTodo.js functionality.

components/AddTodo.js

import React from 'react';

const AddTodo = () => {
    let input;

    const addTask = (e) => {
        e.preventDefault();

        if(!input.value.trim()) return;

        console.log(input.value);

    }
    return (
        <div>
            <form onSubmit={addTask}>
                <input ref={node => input = node}/>
                <button type="submit">
                    Add Todo
                </button>
            </form>
        </div>
    )
}

export default AddTodo;

If you see the above code, I have just added a form with input field and submit button. On form submit, we have logged the input value.

When we run the code, It will look like this

react redux todolist

Now, We need to create an action when a user enters task in input field. This is the action that gets dispatched to update the store using reducer.

Let’s create a file actions.js at root folder and add code for action on input field.

actions.js

let nextTodoId = 0;

export const addTodo = text => ({
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
});

If you see the above code, we have created an action called ‘addTodo’ which returns object with 3 fields.

  1. Type : which tells what type of action we are emitting
  2. Id: unique id for task
  3. Text: input value

We have created an action for input field. Now we need to add it to the component.

Update your components/AddTodo.js with the following code.

components/AddTodo.js

import React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';

const AddTodo = ({dispatch}) => {
    let input;

    const addTask = (e) => {
        e.preventDefault();

        if(!input.value.trim()) return;

        dispatch(addTodo(input.value));
        input.value = '';

    }
    return (
        <div>
            <form onSubmit={addTask}>
                <input ref={node => input = node}/>
                <button type="submit">
                    Add Todo
                </button>
            </form>
        </div>
    )
}

export default connect()(AddTodo);

After updating this, try to run code. You will see an error saying ‘could not find store’.

You will see this error because we didn’t configure the redux in our application.

Let’s go to index.js file and configure the redux.

Index.js

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

import './index.css';
import App from './App';

function test() {
    console.log(test);
}

const store = createStore(test);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>, 

document.getElementById('root'));

If you see the above code, we have imported createStore from redux and Provider from react-redux. We have wrapped our App with Provider and have passed store as prop to the Provider.

We have created store using createStore method. If you notice we have passed a test function as param for createStore. It is supposed to reducers function but we haven’t add any reducers yet. So for the time being, we have created a dummy function and initialized store with that function. It will be changed later.

Now try to run code, You will see no errors and the application runs as usual.

We need to create a reducer function to update the state(store) as per the action emitted.

Let’s create a new file reducers.js and add the following code.

./reducers.js

const todos = (state = [], action) => {
    switch(action.type) {
        case 'ADD_TODO':
            return [
                ...state,
                {
                    id: action.id,
                    text: action.text,
                    completed: false
                }
            ];
        default : return state;    
    }
}

export default todos;

Now update the index.js file

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import todos from './reducers';

import './index.css';
import App from './App';

const store = createStore(todos);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>, 

document.getElementById('root'));

Run the code again and log action and state in reducer, you will see it is getting updated.

So, we successfully used redux to create action when there is a user input.

Now, we need to display the tasks we have added. We are going to implement TodoList component to display the list of tasks.

Before displaying the content in Todolist, let’s get the todolist data first.

So, to get the data in the Todolist component, we need to connect the component with redux.

Please update component/TodoList.js with the following code.

TodoList.js

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

const mapStateToProps = state => {
    return ({
         todos: state 
    })
}

const TodoList = (state) => {
    console.log(state);
    return (
        <div>
            <ul>{
                    state.todos.map((todo) => (
                        <li key={todo.id}>{todo.text}</li>
                    ))
            }
                
            </ul>
        </div>
    )
}

export default connect(mapStateToProps)(TodoList);

if you see the following code, We have created a method called ‘mapStateToProps’. From the name itself you can understand that we are mapping state to props of this component so that the state is available.

Then we are wrapping our component TodoList with connect by passing ‘mapStateToProps’ to make state available.

After this. Reload the webpage, you can see output like this.

react redux todolist

We are able to see list of tasks in TodoList Component.

Now, Let’s add filters and onClick event on Todo task to update the status.

Let’s start with updating the actions.js with following code.

let nextTodoId = 0;

export const addTodo = text => ({
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
});

export const todolistFilter  = filter => ({
    type: 'SET_TODOLIST_FILTER',
    filter
});

export const toggleTodo = id => ({
    type: 'TOGGLE_TODO',
    id
});

export const todolistFilters = {
    SHOW_ALL: 'SHOW_ALL',
    SHOW_COMPLETED: 'SHOW_COMPLETED',
    SHOW_ACTIVE: 'SHOW_ACTIVE'
};

If you see the above code, we have created actions for todolistFilter which sets the filters like active/completed based on user input. We have created action for toggleTodo which updates the status of todo whether it is completed or not.

We have created an object todolistFilters which has constants. We have placed it here so that we can import it where ever we want.

Now, Let’s update the reducers.js file with following code.

import { combineReducers } from 'redux';
import { todolistFilters } from './actions';

const todos = (state = [], action) => {
    switch(action.type) {
        case 'ADD_TODO':
            return [
                ...state,
                {
                    id: action.id,
                    text: action.text,
                    completed: false
                }
            ];
        case 'TOGGLE_TODO':
            return state.map(todo => 
                (todo.id === action.id) ? 
                {...todo, completed: !todo.completed}: 
                todo
            )   
        default : return state;    
    }
}

const todolistFilter = (state=todolistFilters.SHOW_ALL, action) => {
    switch(action.type) {
        case 'SET_TODOLIST_FILTER':
            return action.filter;
        default: return state;    
    }
}

export default combineReducers({
    todos,
    todolistFilter
});

If you see the above code, we have added case ‘TOGGLE_TODO’ in todos, which updates the status of that todo on user action.

We have added a reducer for todolistFilter, which sets the filter on what list to show in TodoList component.

If you notice this file, we have two reducer functions. We need to pass all the reducers together to create a store at root level. To achieve, we have this method called combineReducers in redux, which combines all the reducers. We have passed our two reducers todos and todolist Filter in combineReducers and exported it as default.

We have updated the reducers file. Now we need to update the index.js file as per the new reducers.js exports.

Index.js file will look like this

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './reducers';

import './index.css';
import App from './App';

const store = createStore(rootReducer) || {};

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>, 

document.getElementById('root'));

Now let’s update the TodoList.js file with the following code.

import React from 'react';
import { connect } from 'react-redux';
import { 
    todolistFilters,
    toggleTodo
 } from '../actions';

const getTodoList = (todos, filter) => {
    switch(filter) {
        case todolistFilters.SHOW_ALL:
            return todos;
        case todolistFilters.SHOW_ACTIVE:
            return todos.filter(t => !t.completed);
        case todolistFilters.SHOW_COMPLETED:
            return todos.filter(t => t.completed);
        default: 
            throw new Error('unknown filter' +filter);            
    }
}

const mapStateToProps = state => {
    return ({
         todos: getTodoList(state.todos, state.todolistFilter) 
    })
}

const mapDispatchToProps = dispatch => {
    return {
        toggleTodo: id => dispatch(toggleTodo(id))
    }
}

const TodoList = ({todos, toggleTodo}) => {
    return (
        <div>
            <ul>{
                    todos.map((todo) => (
                        <li 
                            onClick={() => toggleTodo(todo.id)} 
                            key={todo.id}
                            style={{textDecoration: todo.completed ? 'line-through': 'none'}}
                        >
                            
                            {todo.text}
                        </li>
                    ))
            }
                
            </ul>
        </div>
    )
}

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

If you see the above file, we have created a new method called ‘getTodoList’ which returns the list based on filter. We passed it in mapStateToProps where we are setting the state for this component.

We have created a new method called ‘mapDispatchToProps’. This method holds the methods which dispatches on Todolist component actions. We have ‘toggleTodo’ method which updates the state of the task. We are mapping this method to TodoList component.

We are passing this method as prop and dispatching it on click of a single todo.

We have updated the connect method with two props as shown in above code.

Now, Rerun the application in browser, you will see this output.

react redux todolist

We can see the both the tasks which are completed and active together. Now let’s add filters which shows list of tasks based on selected filter.

Let’s start with updating acitons.js. out file will look like this below

actions.js

let nextTodoId = 0;

export const addTodo = text => ({
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
});

export const todolistFilter  = filter => ({
    type: 'SET_TODOLIST_FILTER',
    filter
});

export const toggleTodo = id => ({
    type: 'TOGGLE_TODO',
    id
});

export const setTodoListFilter = filter => ({
    type: 'SET_TODOLIST_FILTER',
    filter
})

export const todolistFilters = {
    SHOW_ALL: 'SHOW_ALL',
    SHOW_COMPLETED: 'SHOW_COMPLETED',
    SHOW_ACTIVE: 'SHOW_ACTIVE'
};

We have added action, setTodoListFilter, which sets the filter type based on click.

We already have added a method ‘todolistFilter’in reducers.js which gets called when action get dispatched.

reducers.js

const todolistFilter = (state=todolistFilters.SHOW_ALL, action) => {
    switch(action.type) {
        case 'SET_TODOLIST_FILTER':
            return action.filter;
        default: return state;    
    }
}

Now let’s make changes in Filters.js

import React from 'react';
import FilterLink from './FilterLink';
import {todolistFilters} from '../actions';

const Filters = () => {

    return (
        <div>
            <span>Show:</span>
            <FilterLink filter={todolistFilters.SHOW_ALL}>All</FilterLink>
            <FilterLink filter={todolistFilters.SHOW_ACTIVE}>Active</FilterLink>
            <FilterLink filter={todolistFilters.SHOW_COMPLETED}>Completed</FilterLink>
        </div>
    )
}

export default Filters;

In the above code, I have imported a component called ‘FilterLink’ and rendered it inside Filters component. We have passed filter as prop which tells what to show.

Now let’s implement ‘FilterLink’ component.

components/FilterLink.js

import React from 'react';
import { connect } from 'react-redux';
import { setTodoListFilter } from '../actions';

const mapStateToProps = (state, ownProps) => ({
    active: ownProps.filter === state.todolistFilter
});

const mapDispatchToProps = (dispatch, ownProps) => ({
    onClick: () => dispatch(setTodoListFilter(ownProps.filter))
})

const FilterLink = ({active, children, onClick}) => {
    return (
        <button
            onClick ={onClick}
            disabled={active}
            style={{marginLeft:'4px'}}
        >
            {children}
        </button>
    )
}

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

From the above code, we are setting up value for ‘active’ based on the filter prop in ‘mapStateToProps’ and we are creating a method ‘onClick’ which dispatches action with its corresponding reducer function.

Save all the files and re run the browser. You will see like this.

react redux todolist

react redux todolist

react redux todolist

So In this way, we can add redux to our react application.

Leave a Comment