DEV Community

Shiva Bhusal
Shiva Bhusal

Posted on • Originally published at shivab.com

Step to step guide to build a TODO app with React and Redux

Introduction

You can get the source code here https://github.com/shivabhusal/todo-app-with-react-and-redux

React

React is spreading like a wildfire in Web Development Industry, as per
StackOverflow survey 2019.


React Survey - Stackoverflow

React is used to build modern web UI in more manageable way. Moreover, I can say that now we
can utilize our programming skills to build UIs. Back in the days, UI developers used to
be the people with HTML / CSS / Bootstrap / Photoshop skill set. Those days, we didn't used to call UI development
a programming job.

Redux

Redux is tool to manage your application state more manageable and usable way you can imagine. Without
it you would have to pass data as props deep down to the lower level components.

Basic Idea of Component Based Design

react-components-diagram

Lets start building a TODO app

We will use create-react-app tool to create a new app. It will configure all necessary tool-chain
like babel, webpack and other.

npx create-react-app todo-app-with-redux

yes its npx, (npx is a package runner tool that comes with npm 5.2+ and higher, see instructions for older npm versions)

File Structures

src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js

Components

// src/components/visibilityFilter.jsx
import React from 'react'

export default function VisibilityFilter({ filters }) {
    return (
        filters.map((filter,i) => <button key={`filter-${i}`}>{filter}</button>)
    )
}
// src/components/todoList.jsx

import React from 'react'

const Todo = ({ todo }) => <li>{todo.content}</li>

export default function TodoList({ todos }) {
    return (
        todos.map((todo, i) => (
            <Todo key={i} todo={todo} />
        ))
    )

}
// src/components/addTodo.jsx

import React from 'react'

export default function addTodo() {
    return (
        <>
            <input type="text" placeholder="You text here" />
            <button>Add</button>
        </>
    )
}
// src/App.jsx

import React from 'react';
import './App.css';
import AddTodo from './components/addTodo'
import TodoList from './components/todoList'
import VisibilityFilter from './components/visibilityFilter'

const filters = ['all', 'completed', 'incomplete']
function App() {
  return (
    <div className="App">
      <h1>TODO Managers</h1>
      <AddTodo/>
      <TodoList todos={[{content: 'Task 1'}, {content: 'Task 2'}]}/>
      <VisibilityFilter filters={filters}/>
    </div>
  );
}

export default App;

Now, since UI of the app is ready its time to make it alive.

Introducing Redux

Now, we need to introduce Redux to feed data to the components and actions. We could
have used React Context API but its more easier to use Redux and pass action around.

We need the following new components:-

  • Action Types : Types of all possible actions
  • Action Creators : Functions those take payload and prepare object with type and payload
  • Reducers : Functions that knows what to do with that action type.
  • Containers are created using connect() function which exposes relevant state and action function.
  • Store : Application state; created using createStore() function that takes, combined reducers as arguments.
  • Provider wraps the entire app and it exposes store to the app.
// src/redux/actions.js

import { ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER } from './actionTypes'

export const addTodo = (content) => (
    {
        type: ADD_TODO,
        payload: {
            content
        }
    }
)

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

export const deleteTodo = (id) => (
    {
        type: DELETE_TODO,
        payload: {
            id
        }
    }
)

export const setFilter = (filter) => (
    {
        type: SET_FILTER,
        payload: {
            filter
        }
    }
)


// src/App.css

button.active{
  background-color: red;
}

li.completed{
  text-decoration: line-through;
}
// src/redux/actionTypes.js

export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const DELETE_TODO = 'DELETE_TODO'
export const SET_FILTER = 'SET_FILTER'

export const FILTER_ALL = 'all'
export const FILTER_COMPLETED = 'completed'
export const FILTER_INCOMPLETE = 'incomplete'
export const Filters = [FILTER_ALL, FILTER_COMPLETED, FILTER_INCOMPLETE]
// src/redux/reducers.js

import {FILTER_ALL} from './actionTypes'
import { ADD_TODO, TOGGLE_TODO, DELETE_TODO, SET_FILTER } from './actionTypes'

const initialTodoState = {
    nextId: 2,
    data:
    {
        1: {
            content: 'Content 1',
            completed: false
        }
    }
}

export const todos = (state = initialTodoState, action) => {
    switch (action.type) {
        case ADD_TODO: {
            return (
                {
                    ...state,
                    data: {
                        ...state.data,
                        [state.nextId]: {
                            completed: false,
                            content: action.payload.content
                        },
                    },

                    nextId: state.nextId + 1
                }
            )
        }
        case TOGGLE_TODO:{
            console.log(action.payload)
            return(
                {
                    ...state,
                    data:{
                        ...state.data,
                        [action.payload.id]:{
                            ...state.data[action.payload.id],
                            completed: !(state.data[action.payload.id].completed)
                        }
                    }
                }
            )
        }

        default: {
            return state
        }
    }
}


export const visibilityFilter = (state = {activeFilter: FILTER_ALL}, action) => {
    switch (action.type) {
        case SET_FILTER: {
            return ({
                activeFilter: action.payload.filter
            })
        }

        default: {
            return state;
        }
    }
}
// src/redux/store.js

import {createStore, combineReducers} from 'redux'
import {todos, visibilityFilter} from './reducers'

export default createStore(combineReducers({todos, visibilityFilter}), 
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
// src/App.jsx

import React from 'react';
import './App.css';
import AddTodo from './components/addTodo'
import TodoList from './components/todoList'
import VisibilityFilter from './components/visibilityFilter'
import {Provider} from 'react-redux'
import store from './redux/store'

function App() {
  return (
    <Provider store={store}>
    <div className="App">
      <h1>TODO Managers</h1>
      <AddTodo/>
      <TodoList/>
      <VisibilityFilter/>
    </div>
    </Provider>
  );
}

export default App;


Updating the Ui Components to connect with Store

// src/components/addTodo.jsx

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

function AddTodo({ addTodo }) {
    const [value, setValue] = useState('');

    const handleOnChange = (e) => {
        setValue(e.target.value)
    }
    const handleAdd = () => {
        setValue('')
        addTodo(value)
    }

    return (
        <>
            <input type="text" onChange={handleOnChange} value={value} placeholder="You text here" />
            <button onClick={handleAdd}>Add</button>
        </>
    )
}

export default connect(null, { addTodo })(AddTodo)

// src/components/todoList.jsx

import React from 'react'
import { connect } from 'react-redux'
import { _ } from 'underscore'
import { FILTER_ALL, FILTER_COMPLETED } from '../redux/actionTypes'
import { toggleTodo } from '../redux/actions'

const Todo = ({ todo, id, toggleTodo }) => (
    <li className={todo.completed ? 'completed' : ''} onClick={() => toggleTodo(id)}>{todo.content}</li>
)

function TodoList({ todos, toggleTodo }) {
    return (
        _.keys(todos).map((id) => (
            <Todo key={id} id={id} toggleTodo={toggleTodo} todo={todos[id]} />
        ))
    )
}

const mapState = (state) => {
    if (state.visibilityFilter.activeFilter === FILTER_ALL) {
        return { todos: state.todos.data }
    } else if (state.visibilityFilter.activeFilter === FILTER_COMPLETED) {
        return ({
            todos: _.pick(state.todos.data, (todo) => todo.completed)
        })
    } else {
        return ({
            todos: _.pick(state.todos.data, (todo) => !todo.completed)
        })
    }
}

export default connect(mapState, { toggleTodo })(TodoList);
// src/components/visibilityFilter.jsx

import React from 'react'
import { connect } from 'react-redux'
import { setFilter } from '../redux/actions'
import { Filters } from '../redux/actionTypes'

function VisibilityFilter({ activeFilter, setFilter }) {
    return (
        Filters.map((filter, i) => (
            <button
                className={filter === activeFilter ? 'active' : ''}
                onClick={() => setFilter(filter)}
                key={`filter-${i}`}>
                {filter}
            </button>
        ))
    )
}

const mapState = (state) => ({ activeFilter: state.visibilityFilter.activeFilter })
export default connect(mapState, { setFilter })(VisibilityFilter)

Read more from me:- https://shivab.com/blog/

Top comments (0)