Mern Stack Crud App Using create-react-app & React Redux Part 2.

Bipin Swarnkar
27 min readSep 3, 2017

--

This is Part 2 of Mern Stack Crud App. Read Part 1.

Step 2. Create the React App with redux

The second thing we will do is to creates a frontend build pipeline with react.js, so you can use it with the express server. Create React App is the best way to start building a new React single page application. It sets up your development environment so that you can use the latest JavaScript features, provides a nice developer experience, and optimizes your app for production.

First things first, make sure you have create-react-app installed if you don’t already:

npm install -g create-react-app

Then by using create-react-app, lets create our new React single page application.

create-react-app react-redux-client

First of all we need to set up the redux and client side routing for our web application. Let’s install the dependencies.

npm install redux react-redux redux-thunk react-router@^3.0.0 react-router-redux react-bootstrap react-router-bootstrap@rr-v3 --save

redux is a library that helps you manage your app’s state. React bindings are not included in Redux by default. You need to install react-redux to use redux with react.js. redux-thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. react-router is a router library for react, it connects nicely to react and redux to help your app support routes. react-router-redux library helps you keep that bit of state in sync with your Redux store.

Lets do a bit of cleanup of unnecessary files to start.

cd src && rm App.test.js App.css favicon.ico index.css

Now create some new folders in the src directory.

mkdir actions components containers reducers store

To explain the different parts:

  • actions are plain JavaScript objects that send data from your application to your store.
  • components let you split the UI into independent, reusable pieces, and think about each piece in isolation.
  • A container component is a component that is responsible for retrieving data, and in order to get that data, the component needs to use Redux’s connect and mapStateToProps functions.
  • reducers are used to update the state object in your store. Just like actions, your application can have multiple reducers.
  • A store holds the whole state tree of your application.
    The only way to change the state inside it is to dispatch an action on it. A store is not a class. It’s just an object with a few methods on it.
    To create it, pass your root reducing function to createStore.

Okay, so lets get started!

Redux Store

First thing we will do is to configure our redux store. Let’s create a new file configureStore.js inside your src/store. Put the following code in it.

./react-redux-client/src/store/configureStore.js

// ./react-redux-client/src/store/configureStore.js
import {createStore, compose, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
const middlewares = [
thunk,
];
const store = createStore(rootReducer, initialState, compose(
applyMiddleware(...middlewares),
window.devToolsExtension ? window.devToolsExtension() : f => f // add support for Redux dev tools
)
);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextReducer = require('../reducers').default; // eslint-disable-line global-require
store.replaceReducer(nextReducer);
});
}
return store;
}

Open up ./react-redux-client/src/App.js put the following code in it.

// ./react-redux-client/src/App.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import PropTypes from 'prop-types';
import { syncHistoryWithStore } from 'react-router-redux';
import configureStore from './store/configureStore';
import routes from './routes';
const store = configureStore();
const history = syncHistoryWithStore(browserHistory, store);
class App extends Component {
render() {
return (
<Provider store={store}>
<div>
<Router history={history} routes={routes} />
</div>
</Provider>
);
}
}
App.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}
export default App;

In the above code we are importing all of the modules from downloaded packages. We import browserHistory from react-router. This object contains functionality that lets us manipulate routes. PropTypes improve the reusability of your component by validating the received data. syncHistoryWithStore is a function from react-router-redux that syncs the browser history with redux’ store. This library helps redux work with react-router. Every route change will equal to an action dispatched to the store. configureStore is a helper function which will help us configure the store.

Routes: Let’s now create component routes for this application.

./react-redux-client/src/routes.js

// ./react-redux-client/src/routes.js
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './containers/App';
import Todos from './containers/Todos';
import Todo from './containers/Todo';
export default (
<Route path="/" component={App}>
<IndexRoute component={Todos} />
<Route path="/:id" component={Todo} />
</Route>
)

Our routes.js is pretty simple: our root path belongs to the App component. React will only render Todos if we are in the root path “/”.Then we have one route “/:id” inside root route, which will render Todo by the id parameter.

Actions

As discussed above Actions are object payloads that are identified by a required property type .Action creators are methods that wrap and return the action object. At the moment, we just need two actions to perform our crud functions. One is appActions.js and other is todoActions.js. Let’s create these two files in a new folder called actions.

./react-redux-client/src/actions/appActions.js

// ./react-redux-client/src/actions/appActions.jsexport const toggleAddTodo = () => {
return {
type: 'TOGGLE_ADD_TODO'
}
}

./react-redux-client/src/actions/todoActions.js

// ./react-redux-client/src/actions/todoActions.jsconst apiUrl = "/api/";export const toggleAddBook = () => {
return {
type: 'TOGGLE_ADD_TODO'
}
}
export const addNewTodo = (todo) => {}export const deleteTodo = (todo) => {}export const editTodo = (todo) => {}//Async action
export const fetchTodos = () => {
// Returns a dispatcher function
// that dispatches an action at later time
return (dispatch) => {
dispatch(fetchTodosRequest());
// Returns a promise
return fetch(apiUrl)
.then(response => {
if(response.ok){
response.json().then(data => {
dispatch(fetchTodosSuccess(data.todos,data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodosFailed(error));
})
}
})
}
}
export const fetchTodosRequest = () => {
return {
type:'FETCH_TODOS_REQUEST'
}
}
//Sync action
export const fetchTodosSuccess = (todos,message) => {
return {
type: 'FETCH_TODOS_SUCCESS',
todos: todos,
message: message,
receivedAt: Date.now
}
}
export const fetchTodosFailed = (error) => {
return {
type:'FETCH_TODOS_FAILED',
error
}
}
export const fetchTodoById = (todoId) => {
return (dispatch) => {
dispatch(fetchTodoRequest());
// Returns a promise
return fetch(apiUrl + todoId)
.then(response => {console.log(response)
if(response.ok){
response.json().then(data => {
dispatch(fetchTodoSuccess(data.todo[0], data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodoFailed(error));
})
}
})
}
}
export const fetchTodoRequest = () => {
return {
type:'FETCH_TODO_REQUEST'
}
}
//Sync action
export const fetchTodoSuccess = (todo,message) => {
return {
type: 'FETCH_TODO_SUCCESS',
todo: todo,
message: message,
receivedAt: Date.now
}
}
export const fetchTodoFailed = (error) => {
return {
type:'FETCH_TODO_FAILED',
error
}
}

Containers

If you’re asking yourself why our components are divided into the /components and /containers directories. Then have a look at this part of the redux docs. Basically, the components directory holds the “dumb” components, those that are not aware of redux and its store. They only receive props and care about presentation. The components inside /containers are aware of the app’s state as they are connected to the redux store.

Now we have to create three container components App.js , Todos.js and Todo.js. Let’s create these files.

./react-redux-client/src/containers/App.js

// ./react-redux-client/src/containers/App.js
import { connect } from 'react-redux';
import * as appActions from '../actions/appActions';
import App from '../components/App';
// map state from store to props
const mapStateToProps = (state) => {
return {
//you can now say this.props.mappedAppSate
mappedAppState: state.appState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
mappedToggleAddTodo: () => dispatch(appActions.toggleAddTodo())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(App);

./react-redux-client/src/containers/Todos.js

// ./react-redux-client/src/containers/Todos.js
import { connect } from 'react-redux';
import * as todoActions from '../actions/todoActions';
import Todos from '../components/Todos';
// map state from store to props
const mapStateToProps = (state,ownProps) => {
return {
//you can now say this.props.mappedAppSate
mappedTodoState: state.todoState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
fetchTodos: () => dispatch(todoActions.fetchTodos()),
mappedDeleteTodo: todoToDelete => dispatch(todoActions.deleteTodo(todoToDelete)),
mappedEditTodo: todoToEdit => dispatch(todoActions.editTodo(todoToEdit))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Todos);

./react-redux-client/src/containers/Todo.js

// ./react-redux-client/src/containers/Todo.js
import { connect } from 'react-redux';
import * as todoActions from '../actions/todoActions';
import Todo from '../components/Todo';
// map state from store to props
const mapStateToProps = (state) => {
return {
//you can now say this.props.mappedAppSate
mappedTodoState: state.todoState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
mappedfetchTodoById: todoId => dispatch(todoActions.fetchTodoById(todoId))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Todo);

We import connect from react-redux. This function connects or subscribes a component to the store, making it aware of any changes to the state. Then we are exporting a wrapper, the wrapper takes 2 params when being created: mapStateToProps, and mapDispatchToProps.

With these 2 params, you can map which part of the state you want to make available to the component , as well as which actions.

Components

Now come to components App.js, Todo.js and Todos.js we had imported in our containers. Component’s only concern is to show the todo data . Let’s create these components one by one.

./react-redux-client/src/components/App.js

// ./react-redux-client/src/components/App.js
import React from 'react';
import { Navbar,Nav,NavItem,MenuItem } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import './App.css';
export default class App extends React.Component {
constructor(props){
super(props);
this.toggleAddTodo = this.toggleAddTodo.bind(this);
}
toggleAddTodo(e){
e.preventDefault();
this.props.mappedToggleAddTodo();
}
render(){
return(
<div>
<Navbar inverse collapseOnSelect className="customNav">
<Navbar.Header>
<Navbar.Brand>
<a href="/#">Mern Stack Todo App</a>
</Navbar.Brand>
<Navbar.Toggle />
</Navbar.Header>
<Navbar.Collapse>
<Nav>
<LinkContainer to={{ pathname: '/', query: { } }}>
<NavItem eventKey={1}>Home</NavItem>
</LinkContainer>
</Nav>
<Nav pullRight>
<LinkContainer to={{ pathname: '/', query: { } }} onClick={this.toggleAddTodo}>
<NavItem eventKey={1}>Add Todo</NavItem>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Navbar>
<div className="container">
{ /* Each Smaller Components */}
{this.props.children}
</div>
</div>
);
}
}

./react-redux-client/src/components/Todos.js

// ./react-redux-client/src/components/Todos.js
import React from 'react';
import { Alert,Glyphicon,Button,Modal } from 'react-bootstrap';
import { Link } from 'react-router';
export default class Todos extends React.Component {
constructor(props){
super(props);
}
componentWillMount(){
this.props.fetchTodos();
}
showEditModal(bookToEdit){
//this.props.mappedshowEditModal(todoToEdit);
}
hideEditModal(){
//this.props.mappedhideEditModal();
}
hideDeleteModal(){
//this.props.mappedhideDeleteModal();
}
showDeleteModal(todoToDelete){
//this.props.mappedshowDeleteModal(todoToDelete);
}
render(){
const todoState = this.props.mappedTodoState;
const todos = todoState.todos;
return(
<div className="col-md-12">
<h3 className="centerAlign">Todos</h3>
{!todos && todoState.isFetching &&
<p>Loading todos....</p>
}
{todos.length <= 0 && !todoState.isFetching &&
<p>No Todos Available. Add A Todo to List here.</p>
}
{todos && todos.length > 0 && !todoState.isFetching &&
<table className="table booksTable">
<thead>
<tr><th>Todo</th><th className="textCenter">Edit</th><th className="textCenter">Delete</th><th className="textCenter">View</th></tr>
</thead>
<tbody>
{todos.map((todo,i) => <tr key={i}>
<td>{todo.todoText}</td>
<td className="textCenter"><Button onClick={() => this.showEditModal(todo)} bsStyle="info" bsSize="xsmall"><Glyphicon glyph="pencil" /></Button></td>
<td className="textCenter"><Button onClick={() => this.showDeleteModal(todo)} bsStyle="danger" bsSize="xsmall"><Glyphicon glyph="trash" /></Button></td>
<td className="textCenter"><Link to={`/${todo._id}`}>View Details</Link> </td>
</tr> )
}
</tbody>
</table>
}
</div>
);
}
}

./react-redux-client/src/components/Todo.js

// ./react-redux-client/src/components/Todo.js
import React from 'react';
export default class Todo extends React.Component {
componentDidMount(){
this.props.mappedfetchTodoById(this.props.params.id);
}
render(){
const todoState = this.props.mappedTodoState;
return(
<div className="todoDetail">
<h2>Todo Detail</h2>
{!todoState.todo && todoState.isFetching &&
<div>
<p>Loading todo....</p>
</div>
}
{todoState.todo && !todoState.isFetching &&
<div>
<h3>{todoState.todo.todoText}</h3>
<p>{todoState.todo.todoDesc}</p>
</div>
}
</div>
);
}
}

In the above components we have used react-bootstrap. The most popular front-end framework, rebuilt for React.

Reducers

Reducers are used to update the state object in your store. Actions describe the fact that something happened, but don’t specify how the application’s state changes in response. This is the job of reducers.

Let’s create our reducers .

./react-redux-client/src/reducers/appReducer.js

// ./react-redux-client/src/reducers/appReducer.js
const INITIAL_STATE = {
showAddTodo: false
}
const appReducer = (currentState = INITIAL_STATE, action) => {
switch (action.type) {
case 'TOGGLE_ADD_TODO':
return {
...currentState,showAddTodo: !currentState.showAddTodo
}
default:
return currentState;
}
}
export default appReducer;

./react-redux-client/src/reducers/todoReducer.js

// ./react-redux-client/src/reducers/todoReducer.js
const INITIAL_STATE = {
todos:[],
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
export const todoReducer = (currentState = INITIAL_STATE, action) => {
switch (action.type) {
case 'FETCH_TODOS_REQUEST':
return {
...currentState,
todos:[],
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_SUCCESS':
return {
...currentState,
todos:action.todos,
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_SUCCESS':
return {
...currentState,
todos:currentState.todos,
todo:action.todo,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
default:
return currentState;
}
}

When the store dispatched an action, all the reducers are called. So who do we know which action to act on? By using a Switch statement, we determine which action was dispatched and act on that.

combineReducers

combineReducers is a utility function from redux. It combines two or more reducers and unites them under the single state tree and the combined reducer is the root reducer. Let’s create our root reducer index.js.

./react-redux-client/src/reducers/index.js

// ./react-redux-client/src/reducers/index.js
import { routerReducer as routing } from 'react-router-redux';
import { combineReducers } from 'redux';
import appReducer from './appReducer';
import todoReducer from './todoReducer';
export default combineReducers({
appState:appReducer,
todoState:todoReducer,
routing
// More reducers if there are
// can go here
})

Now start your react.js app using npm. Don’t forget to start your express server app and mongoDB instance before starting the react.js app. If you haven’t read the express server part please read it first.

Start the react app.

npm start

You can see the home page with no todos as given below.

Great we just finished the basic structure of our react redux app let’s start our crud functionality Add Todo, Delete Todo and Edit Todo.

As of now we have given a add todo nav button, not working till now, but we will make it working in our next Add Todo Data section.

Add Todo Data

We need a Todo form to submit todo. Let’s create a new form component and include it to our app.

./react-redux-client/src/components/TodoForm.js

// ./react-redux-client/src/components/TodoForm.js
import React from 'react';
import { FormGroup,ControlLabel,FormControl,Button } from 'react-bootstrap';
const TodoForm = (props) => {
return (
<form className="form form-horizontal" id="addTodoForm" onSubmit={props.addTodo}>
<div className="row">
<h3 className="centerAlign">Add Your Todo</h3>
<div className="col-md-12">
<FormGroup>
<ControlLabel>Todo: </ControlLabel>
<FormControl
type="text" placeholder="Enter todo"
name="todoText"
/>
</FormGroup>
</div>
<div className="col-md-12">
<FormGroup>
<ControlLabel>Description: </ControlLabel>
<FormControl
componentClass="textarea" placeholder="Enter description"
name="todoDesc"
/>
</FormGroup>
</div>
</div>
<FormGroup>
<Button type="submit" bsStyle="success" bsSize="large" block>Submit</Button>
</FormGroup>
</form>
);
}
export default TodoForm;

In the above TodoForm component we are calling the addTodo method from parent component to handle form submission.

All we just need to do is to import the TodoForm component and inject into the App.js component. The key line is the one below:

<TodoForm addTodo={this.addTodo} />

After injecting Todo and adding addTodo method for validating and sending data to the server by dispatching a AddNewTodo action, the whole App.js will look like given below.

./react-redux-client/src/components/App.js

// ./react-redux-client/src/components/App.js
import React from 'react';
import { Navbar,Nav,NavItem,MenuItem } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import './App.css';
import TodoForm from './TodoForm';
export default class App extends React.Component {
constructor(props){
super(props);
this.toggleAddTodo = this.toggleAddTodo.bind(this);
this.addTodo = this.addTodo.bind(this);
}
toggleAddTodo(e){
e.preventDefault();
this.props.mappedToggleAddTodo();
}
addTodo(e){
e.preventDefault();
const form = document.getElementById('addTodoForm');
if(form.todoText.value !== "" && form.todoDesc.value !== ""){
const data = new FormData();
data.append('todoText', form.todoText.value);
data.append('todoDesc', form.todoDesc.value);
this.props.mappedAddTodo(data);
}
else{
return ;
}
}
render(){
const appState = this.props.mappedAppState;
return(
<div>
<Navbar inverse collapseOnSelect className="customNav">
<Navbar.Header>
<Navbar.Brand>
<a href="/#">Mern Stack Todo App</a>
</Navbar.Brand>
<Navbar.Toggle />
</Navbar.Header>
<Navbar.Collapse>
<Nav>
<LinkContainer to={{ pathname: '/', query: { } }}>
<NavItem eventKey={1}>Home</NavItem>
</LinkContainer>
</Nav>
<Nav pullRight>
<LinkContainer to={{ pathname: '/', query: { } }} onClick={this.toggleAddTodo}>
<NavItem eventKey={1}>Add Todo</NavItem>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Navbar>
<div className="container">
{appState.showAddTodo &&
<TodoForm addTodo={this.addTodo} />
}
{ /* Each Smaller Components */}
{this.props.children}
</div>
</div>
);
}
}

Now we need to add some more actions in todoActions.js . After adding new actions whole file will be.

./react-redux-client/src/actions/todoActions.js

// ./react-redux-client/src/actions/todoActions.jsconst apiUrl = "/api/";export const toggleAddBook = () => {
return {
type: 'TOGGLE_ADD_TODO'
}
}
export const addNewTodo = (todo) => {console.log(todo)
return (dispatch) => {
dispatch(addNewTodoRequest(todo));
return fetch(apiUrl, {
method:'post',
// headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: todo,
}).then(response => {
if(response.ok){
response.json().then(data => {
dispatch(addNewTodoRequestSuccess(data.todo[0], data.message))
})
}
else{
response.json().then(error => {
dispatch(addNewTodoRequestFailed(error))
})
}
})
}
}
export const addNewTodoRequest = (todo) => {
return {
type: 'ADD_NEW_TODO_REQUEST',
todo
}
}
export const addNewTodoRequestSuccess = (todo,message) => {
return {
type: 'ADD_NEW_TODO_REQUEST_SUCCESS',
todo:todo,
message:message
}
}
export const addNewTodoRequestFailed = (error) => {
return {
type: 'ADD_NEW_TODO_REQUEST_FAILED',
error
}
}
export const deleteTodo = (todo) => {}export const editTodo = (todo) => {}//Async action
export const fetchTodos = () => {
// Returns a dispatcher function
// that dispatches an action at later time
return (dispatch) => {
dispatch(fetchTodosRequest());
// Returns a promise
return fetch(apiUrl)
.then(response => {
if(response.ok){
response.json().then(data => {
dispatch(fetchTodosSuccess(data.todos,data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodosFailed(error));
})
}
})
}
}
export const fetchTodosRequest = () => {
return {
type:'FETCH_TODOS_REQUEST'
}
}
//Sync action
export const fetchTodosSuccess = (todos,message) => {
return {
type: 'FETCH_TODOS_SUCCESS',
todos: todos,
message: message,
receivedAt: Date.now
}
}
export const fetchTodosFailed = (error) => {
return {
type:'FETCH_TODOS_FAILED',
error
}
}
export const fetchTodoById = (todoId) => {
return (dispatch) => {
dispatch(fetchTodoRequest());
// Returns a promise
return fetch(apiUrl + todoId)
.then(response => {console.log(response)
if(response.ok){
response.json().then(data => {
dispatch(fetchTodoSuccess(data.todo[0], data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodoFailed(error));
})
}
})
}
}
export const fetchTodoRequest = () => {
return {
type:'FETCH_TODO_REQUEST'
}
}
//Sync action
export const fetchTodoSuccess = (todo,message) => {
return {
type: 'FETCH_TODO_SUCCESS',
todo: todo,
message: message,
receivedAt: Date.now
}
}
export const fetchTodoFailed = (error) => {
return {
type:'FETCH_TODO_FAILED',
error
}
}

Every time you want to change data, you must go through a few steps. You will need to call an action creator that returns an action. The store then sends the action to the reducers. Many times, you’ll want your action creator to be a function, which will allow you to do added logic, like http requests, etc. You do this through Middleware, which provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

So Let’s add some new code in todoReducer.js to takes the state and an action before adding a new todo, and returns the next state. After adding the todoReducer.js.

./react-redux-client/src/reducers/todoReducer.js

// ./react-redux-client/src/reducers/todoReducer.js
const INITIAL_STATE = {
todos:[],
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
export const todoReducer = (currentState = INITIAL_STATE, action) => {
switch (action.type) {
case 'FETCH_TODOS_REQUEST':
return {
...currentState,
todos:[],
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_SUCCESS':
return {
...currentState,
todos:action.todos,
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_SUCCESS':
return {
...currentState,
todos:currentState.todos,
todo:action.todo,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'ADD_NEW_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: action.todo
}
case 'ADD_NEW_TODO_REQUEST_FAILED':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'ADD_NEW_TODO_REQUEST_SUCCESS':
return {
...currentState,
todos:[...currentState.todos, action.todo],
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: action.todo
}
default:
return currentState;
}
}

Now Final step to complete the Add Todo module is to dispatch the AddNewTodo action on form submit . Let’s modify our App.js Container component.

./react-redux-client/src/containers/App.js

// ./react-redux-client/src/containers/App.js
import { connect } from 'react-redux';
import * as appActions from '../actions/appActions';
import App from '../components/App';
import * as todoActions from '../actions/todoActions';
// map state from store to props
const mapStateToProps = (state) => {
return {
//you can now say this.props.mappedAppSate
mappedAppState: state.appState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
mappedToggleAddTodo: () => dispatch(appActions.toggleAddTodo()),
mappedAddTodo: todo => dispatch(todoActions.addNewTodo(todo))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(App);

Now as soon as you add a todo, it is immediately added to the todos list!

Edit Todo Data

Let’s start the second module of this react app, which is to allow user to modify his todos. We need to create a Todo Edit Form Component and a dynamic box to show this form in the Todos page, so for this dynamic box we will use Modal from React-Bootstrap.

./react-redux-client/src/components/TodoEditForm.js

// ./react-redux-client/src/components/TodoEditForm.js
import React from 'react';
import { FormGroup,ControlLabel,FormControl,Button } from 'react-bootstrap';
const TodoEditForm = (props) => {
return (
<form className="form form-horizontal" id="EditTodoForm" onSubmit={props.editTodo}>
<div className="row">
<div className="col-md-12">
<FormGroup>
<ControlLabel>Todo: </ControlLabel>
<input type="hidden" value={props.todoData._id} name="id"/>
<FormControl
type="text" placeholder="Enter todo"
name="todoText" defaultValue={props.todoData.todoText}
/>
</FormGroup>
</div>
<div className="col-md-12">
<FormGroup>
<ControlLabel>Description: </ControlLabel>
<FormControl
componentClass="textarea" placeholder="Enter description"
name="todoDesc" defaultValue={props.todoData.todoDesc}
/>
</FormGroup>
</div>
</div>
<FormGroup>
<Button type="submit" bsStyle="success" bsSize="large" block>Submit</Button>
</FormGroup>
</form>
);
}
export default TodoEditForm;

The above Edit Todo Form component needs to be imported and wrapped in the Bootstrap Edit Modal . Alse we have to add a new method submitEditTodoto to the parent component class Todos.js to dispatch the mappedEditTodo.

./react-redux-client/src/components/Todos.js

// ./react-redux-client/src/components/Todos.js
import React from 'react';
import { Alert,Glyphicon,Button,Modal } from 'react-bootstrap';
import { Link } from 'react-router';
import TodoEditForm from './TodoEditForm';
export default class Todos extends React.Component {
constructor(props){
super(props);
this.hideEditModal = this.hideEditModal.bind(this);
this.submitEditTodo = this.submitEditTodo.bind(this);
}
componentWillMount(){
this.props.fetchTodos();
}
showEditModal(todoToEdit){
this.props.mappedshowEditModal(todoToEdit);
}
hideEditModal(){
this.props.mappedhideEditModal();
}
submitEditTodo(e){
e.preventDefault();
const editForm = document.getElementById('EditTodoForm');
if(editForm.todoText.value !== ""){
const data = new FormData();
data.append('id', editForm.id.value);
data.append('todoText', editForm.todoText.value);
data.append('todoDesc', editForm.todoDesc.value);
this.props.mappedEditTodo(data);
}
else{
return;
}
}hideDeleteModal(){
//this.props.mappedhideDeleteModal();
}
showDeleteModal(todoToDelete){
//this.props.mappedshowDeleteModal(todoToDelete);
}
render(){
const todoState = this.props.mappedTodoState;
const todos = todoState.todos;
const editTodo = todoState.todoToEdit;
return(
<div className="col-md-12">
<h3 className="centerAlign">Todos</h3>
{!todos && todoState.isFetching &&
<p>Loading todos....</p>
}
{todos.length <= 0 && !todoState.isFetching &&
<p>No Todos Available. Add A Todo to List here.</p>
}
{todos && todos.length > 0 && !todoState.isFetching &&
<table className="table booksTable">
<thead>
<tr><th>Todo</th><th className="textCenter">Edit</th><th className="textCenter">Delete</th><th className="textCenter">View</th></tr>
</thead>
<tbody>
{todos.map((todo,i) => <tr key={i}>
<td>{todo.todoText}</td>
<td className="textCenter"><Button onClick={() => this.showEditModal(todo)} bsStyle="info" bsSize="xsmall"><Glyphicon glyph="pencil" /></Button></td>
<td className="textCenter"><Button onClick={() => this.showDeleteModal(todo)} bsStyle="danger" bsSize="xsmall"><Glyphicon glyph="trash" /></Button></td>
<td className="textCenter"><Link to={`/${todo._id}`}>View Details</Link> </td>
</tr> )
}
</tbody>
</table>
}
{/* Modal for editing todo */}
<Modal
show={todoState.showEditModal}
onHide={this.hideEditModal}
container={this}
aria-labelledby="contained-modal-title"
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title">Edit Your Todo</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="col-md-12">
{editTodo &&
<TodoEditForm todoData={editTodo} editTodo={this.submitEditTodo} />
}
{editTodo && todoState.isFetching &&
<Alert bsStyle="info">
<strong>Updating...... </strong>
</Alert>
}
{editTodo && !todoState.isFetching && todoState.error &&
<Alert bsStyle="danger">
<strong>Failed. {todoState.error} </strong>
</Alert>
}
{editTodo && !todoState.isFetching && todoState.successMsg &&
<Alert bsStyle="success">
Book <strong> {editTodo.todoText} </strong>{todoState.successMsg}
</Alert>
}
</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.hideEditModal}>Close</Button>
</Modal.Footer>
</Modal>
</div>
);
}
}

Map Edit Action editTodo in Container Component Todos.js.

./react-redux-client/src/containers/Todos.js

// ./react-redux-client/src/containers/Todos.js
import { connect } from 'react-redux';
import * as todoActions from '../actions/todoActions';
import Todos from '../components/Todos';
// map state from store to props
const mapStateToProps = (state,ownProps) => {
return {
//you can now say this.props.mappedAppSate
mappedTodoState: state.todoState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
fetchTodos: () => dispatch(todoActions.fetchTodos()),
mappedDeleteTodo: todoToDelete => dispatch(todoActions.deleteTodo(todoToDelete)),
mappedEditTodo: todoToEdit => dispatch(todoActions.editTodo(todoToEdit)),
mappedshowEditModal: todoToEdit => dispatch(todoActions.showEditModal(todoToEdit)),
mappedhideEditModal: () => dispatch(todoActions.hideEditModal())

}
}
export default connect(mapStateToProps,mapDispatchToProps)(Todos);

Now we have to add some Actions in todoActions.js that we have used in our Container Component Todos.js.

./react-redux-client/src/actions/todoActions.js

// ./react-redux-client/src/actions/todoActions.jsconst apiUrl = "/api/";export const toggleAddBook = () => {
return {
type: 'TOGGLE_ADD_TODO'
}
}
export const addNewTodo = (todo) => {console.log(todo)
return (dispatch) => {
dispatch(addNewTodoRequest(todo));
return fetch(apiUrl, {
method:'post',
body: todo,
}).then(response => {
if(response.ok){
response.json().then(data => {console.log(data.todo);
dispatch(addNewTodoRequestSuccess(data.todo, data.message))
})
}
else{
response.json().then(error => {
dispatch(addNewTodoRequestFailed(error))
})
}
})
}
}
export const addNewTodoRequest = (todo) => {
return {
type: 'ADD_NEW_TODO_REQUEST',
todo
}
}
export const addNewTodoRequestSuccess = (todo,message) => {
return {
type: 'ADD_NEW_TODO_REQUEST_SUCCESS',
todo:todo,
message:message
}
}
export const addNewTodoRequestFailed = (error) => {
return {
type: 'ADD_NEW_TODO_REQUEST_FAILED',
error
}
}
export const deleteTodo = (todo) => {}//Async action
export const fetchTodos = () => {
// Returns a dispatcher function
// that dispatches an action at later time
return (dispatch) => {
dispatch(fetchTodosRequest());
// Returns a promise
return fetch(apiUrl)
.then(response => {
if(response.ok){
response.json().then(data => {
dispatch(fetchTodosSuccess(data.todos,data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodosFailed(error));
})
}
})
}
}
export const fetchTodosRequest = () => {
return {
type:'FETCH_TODOS_REQUEST'
}
}
//Sync action
export const fetchTodosSuccess = (todos,message) => {
return {
type: 'FETCH_TODOS_SUCCESS',
todos: todos,
message: message,
receivedAt: Date.now
}
}
export const fetchTodosFailed = (error) => {
return {
type:'FETCH_TODOS_FAILED',
error
}
}
export const fetchTodoById = (todoId) => {
return (dispatch) => {
dispatch(fetchTodoRequest());
// Returns a promise
return fetch(apiUrl + todoId)
.then(response => {console.log(response)
if(response.ok){
response.json().then(data => {
dispatch(fetchTodoSuccess(data.todo[0], data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodoFailed(error));
})
}
})
}
}
export const fetchTodoRequest = () => {
return {
type:'FETCH_TODO_REQUEST'
}
}
//Sync action
export const fetchTodoSuccess = (todo,message) => {
return {
type: 'FETCH_TODO_SUCCESS',
todo: todo,
message: message,
receivedAt: Date.now
}
}
export const fetchTodoFailed = (error) => {
return {
type:'FETCH_TODO_FAILED',
error
}
}
export const showEditModal = (todoToEdit) => {
return {
type:'SHOW_EDIT_MODAL',
todo:todoToEdit
}
}
export const hideEditModal = () => {
return {
type:'HIDE_EDIT_MODAL'
}
}
export const editTodo = (todo) => {
return (dispatch) => {
dispatch(editTodoRequest(todo));
return fetch(apiUrl, {
method:'put',
body:todo
}).then(response => {
if(response.ok){
response.json().then(data => {
dispatch(editTodoSuccess(data.todo,data.message));
})
}
else{
response.json().then(error => {
dispatch(editTodoFailed(error));
})
}
})
}
}
export const editTodoRequest = (todo) => {
return {
type:'EDIT_TODO_REQUEST',
todo
}
}
export const editTodoSuccess = (todo,message) => {
return {
type:'EDIT_TODO_SUCCESS',
todo:todo,
message:message
}
}
export const editTodoFailed = (error) => {
return {
type:'EDIT_TODO_FAILED',
error
}
}

Also some reducers has to be added in todoReducer.js to update the Todo State.

./react-redux-client/src/reducers/todoReducer.js

// ./react-redux-client/src/reducers/todoReducer.js
const INITIAL_STATE = {
todos:[],
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
export const todoReducer = (currentState = INITIAL_STATE, action) => {
switch (action.type) {
case 'FETCH_TODOS_REQUEST':
return {
...currentState,
todos:[],
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_SUCCESS':
return {
...currentState,
todos:action.todos,
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_SUCCESS':
return {
...currentState,
todos:currentState.todos,
todo:action.todo,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'ADD_NEW_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: action.todo
}
case 'ADD_NEW_TODO_REQUEST_FAILED':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'ADD_NEW_TODO_REQUEST_SUCCESS':
const nextState = {
...currentState,
todos:[...currentState.todos, action.todo],
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: action.todo
}
return nextState;
case 'SHOW_EDIT_MODAL':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: action.todo,
newTodo: null
}
case 'HIDE_EDIT_MODAL':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'EDIT_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: action.todo,
newTodo: null
}
case 'EDIT_TODO_SUCCESS':
const updatedTodos = currentState.todos.map((todo) => {
if(todo._id !== action.todo._id){
//This is not the item we care about, keep it as is
return todo;
}
//Otherwise, this is the one we want to return an updated value
return { ...todo, ...action.todo }
})
return {
...currentState,
todos:updatedTodos,
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: action.todo,
newTodo: null
}
case 'EDIT_TODO_FAILED':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: currentState.todoToEdit,
newTodo: null
}
default:
return currentState;
}
}

We just finished our Edit Todo module. Let’s move to Delete Todo.

Delete Todo

Every time you add a new functionality to your app, you need to add some actions and reducers to your action file and reducer file. Here, these files are todoActions.js and todoReducer.js respectively. Let’s add some new code to these files which will avail the user to delete his todos.

./react-redux-client/src/actions/todoActions.js

// ./react-redux-client/src/actions/todoActions.jsconst apiUrl = "/api/";export const toggleAddBook = () => {
return {
type: 'TOGGLE_ADD_TODO'
}
}
export const addNewTodo = (todo) => {console.log(todo)
return (dispatch) => {
dispatch(addNewTodoRequest(todo));
return fetch(apiUrl, {
method:'post',
body: todo,
}).then(response => {
if(response.ok){
response.json().then(data => {console.log(data.todo);
dispatch(addNewTodoRequestSuccess(data.todo, data.message))
})
}
else{
response.json().then(error => {
dispatch(addNewTodoRequestFailed(error))
})
}
})
}
}
export const addNewTodoRequest = (todo) => {
return {
type: 'ADD_NEW_TODO_REQUEST',
todo
}
}
export const addNewTodoRequestSuccess = (todo,message) => {
return {
type: 'ADD_NEW_TODO_REQUEST_SUCCESS',
todo:todo,
message:message
}
}
export const addNewTodoRequestFailed = (error) => {
return {
type: 'ADD_NEW_TODO_REQUEST_FAILED',
error
}
}
//Async action
export const fetchTodos = () => {
// Returns a dispatcher function
// that dispatches an action at later time
return (dispatch) => {
dispatch(fetchTodosRequest());
// Returns a promise
return fetch(apiUrl)
.then(response => {
if(response.ok){
response.json().then(data => {
dispatch(fetchTodosSuccess(data.todos,data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodosFailed(error));
})
}
})
}
}
export const fetchTodosRequest = () => {
return {
type:'FETCH_TODOS_REQUEST'
}
}
//Sync action
export const fetchTodosSuccess = (todos,message) => {
return {
type: 'FETCH_TODOS_SUCCESS',
todos: todos,
message: message,
receivedAt: Date.now
}
}
export const fetchTodosFailed = (error) => {
return {
type:'FETCH_TODOS_FAILED',
error
}
}
export const fetchTodoById = (todoId) => {
return (dispatch) => {
dispatch(fetchTodoRequest());
// Returns a promise
return fetch(apiUrl + todoId)
.then(response => {console.log(response)
if(response.ok){
response.json().then(data => {
dispatch(fetchTodoSuccess(data.todo[0], data.message));
})
}
else{
response.json().then(error => {
dispatch(fetchTodoFailed(error));
})
}
})
}
}
export const fetchTodoRequest = () => {
return {
type:'FETCH_TODO_REQUEST'
}
}
//Sync action
export const fetchTodoSuccess = (todo,message) => {
return {
type: 'FETCH_TODO_SUCCESS',
todo: todo,
message: message,
receivedAt: Date.now
}
}
export const fetchTodoFailed = (error) => {
return {
type:'FETCH_TODO_FAILED',
error
}
}
export const showEditModal = (todoToEdit) => {
return {
type:'SHOW_EDIT_MODAL',
todo:todoToEdit
}
}
export const hideEditModal = () => {
return {
type:'HIDE_EDIT_MODAL'
}
}
export const editTodo = (todo) => {
return (dispatch) => {
dispatch(editTodoRequest(todo));
return fetch(apiUrl, {
method:'put',
body:todo
}).then(response => {
if(response.ok){
response.json().then(data => {
dispatch(editTodoSuccess(data.todo,data.message));
})
}
else{
response.json().then(error => {
dispatch(editTodoFailed(error));
})
}
})
}
}
export const editTodoRequest = (todo) => {
return {
type:'EDIT_TODO_REQUEST',
todo
}
}
export const editTodoSuccess = (todo,message) => {
return {
type:'EDIT_TODO_SUCCESS',
todo:todo,
message:message
}
}
export const editTodoFailed = (error) => {
return {
type:'EDIT_TODO_FAILED',
error
}
}
export const deleteTodo = (todo) => {
return (dispatch) => {
dispatch(deleteTodoRequest(todo));
return fetch(apiUrl + todo._id ,{
method:'delete'
}).then(response => {
if(response.ok){
response.json().then(data => {
dispatch(deleteTodoSuccess(data.message));
})
}
else{
response.json().then(error => {
dispatch(deleteTodoFailed(error));
})
}
})
}
}
export const deleteTodoRequest = (todo) => {
return {
type:'DELETE_TODO_REQUEST',
todo
}
}
export const deleteTodoSuccess = (message) => {
return {
type:'DELETE_TODO_SUCCESS',
message:message
}
}
export const deleteTodoFailed = (error) => {
return {
type:'DELETE_TODO_FAILED',
error
}
}
export const showDeleteModal = (todoToDelete) => {
return {
type:'SHOW_DELETE_MODAL',
todo:todoToDelete
}
}
export const hideDeleteModal = () => {
return {
type:'HIDE_DELETE_MODAL'
}
}

./react-redux-client/src/reducers/todoReducer.js

// ./react-redux-client/src/reducers/todoReducer.js
const INITIAL_STATE = {
todos:[],
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
export const todoReducer = (currentState = INITIAL_STATE, action) => {
switch (action.type) {
case 'FETCH_TODOS_REQUEST':
return {
...currentState,
todos:[],
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_SUCCESS':
return {
...currentState,
todos:action.todos,
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODOS_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_SUCCESS':
return {
...currentState,
todos:currentState.todos,
todo:action.todo,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'FETCH_TODO_FAILED':
return {
...currentState,
todos:[],
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
}
case 'ADD_NEW_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: action.todo
}
case 'ADD_NEW_TODO_REQUEST_FAILED':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'ADD_NEW_TODO_REQUEST_SUCCESS':
const nextState = {
...currentState,
todos:[...currentState.todos, action.todo],
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: action.todo
}
return nextState;
case 'SHOW_EDIT_MODAL':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: action.todo,
newTodo: null
}
case 'HIDE_EDIT_MODAL':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'EDIT_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: action.todo,
newTodo: null
}
case 'EDIT_TODO_SUCCESS':
const updatedTodos = currentState.todos.map((todo) => {
if(todo._id !== action.todo._id){
//This is not the item we care about, keep it as is
return todo;
}
//Otherwise, this is the one we want to return an updated value
return { ...todo, ...action.todo }
})
return {
...currentState,
todos:updatedTodos,
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: action.todo,
newTodo: null
}
case 'EDIT_TODO_FAILED':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: true,
todoToEdit: currentState.todoToEdit,
newTodo: null
}
case 'DELETE_TODO_REQUEST':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: true,
error: null,
successMsg:null,
showDeleteModal: true,
todoToDelete: action.todo,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'DELETE_TODO_SUCCESS':
const filteredTodos = currentState.todos.filter((todo) => todo._id !== currentState.todoToDelete._id)
return {
...currentState,
todos:filteredTodos,
todo:null,
isFetching: false,
error: null,
successMsg:action.message,
showDeleteModal: true,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'DELETE_TODO_FAILED':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: action.error,
successMsg:null,
showDeleteModal: true,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'SHOW_DELETE_MODAL':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: true,
todoToDelete: action.todo,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
case 'HIDE_DELETE_MODAL':
return {
...currentState,
todos:currentState.todos,
todo:null,
isFetching: false,
error: null,
successMsg:null,
showDeleteModal: false,
todoToDelete: null,
showEditModal: false,
todoToEdit: null,
newTodo: null
}
default:
return currentState;
}
}

As we know that, the components inside /containers are aware of the app’s state as they are connected to the redux store. Using mapStateToProps, and mapDispatchToProps we had already mapped our todo state and actions to our Todo component Todos.js. We need to map some more actions to perform Delete Todo event successfully. Let’s add some more stuff.

./react-redux-client/src/containers/Todos.js

// ./react-redux-client/src/containers/Todos.js
import { connect } from 'react-redux';
import * as todoActions from '../actions/todoActions';
import Todos from '../components/Todos';
// map state from store to props
const mapStateToProps = (state,ownProps) => {
return {
//you can now say this.props.mappedAppSate
mappedTodoState: state.todoState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
fetchTodos: () => dispatch(todoActions.fetchTodos()),
mappedEditTodo: todoToEdit => dispatch(todoActions.editTodo(todoToEdit)),
mappedshowEditModal: todoToEdit => dispatch(todoActions.showEditModal(todoToEdit)),
mappedhideEditModal: () => dispatch(todoActions.hideEditModal()),
mappedDeleteTodo: todoToDelete => dispatch(todoActions.deleteTodo(todoToDelete)),
mappedshowDeleteModal: todoToDelete => dispatch(todoActions.showDeleteModal(todoToDelete)),
mappedhideDeleteModal: () => dispatch(todoActions.hideDeleteModal())

}
}
export default connect(mapStateToProps,mapDispatchToProps)(Todos);

mapStateToProps() is a utility which helps your component get updated state (which is updated by some other components).
mapDispatchToProps() is a utility which will help your component to fire an action event (dispatching action which may cause change of application state).

Now our final Todos.js component with some new methods dealing with delete events, modal for confirmation on delete will look like given below.

./react-redux-client/src/components/Todos.js

// ./react-redux-client/src/components/Todos.js
import React from 'react';
import { Alert,Glyphicon,Button,Modal } from 'react-bootstrap';
import { Link } from 'react-router';
import TodoEditForm from './TodoEditForm';
export default class Todos extends React.Component {
constructor(props){
super(props);
this.hideEditModal = this.hideEditModal.bind(this);
this.submitEditTodo = this.submitEditTodo.bind(this);
this.hideDeleteModal = this.hideDeleteModal.bind(this);
this.cofirmDeleteTodo = this.cofirmDeleteTodo.bind(this);

}
componentWillMount(){
this.props.fetchTodos();
}
showEditModal(todoToEdit){
this.props.mappedshowEditModal(todoToEdit);
}
hideEditModal(){
this.props.mappedhideEditModal();
}
submitEditTodo(e){
e.preventDefault();
const editForm = document.getElementById('EditTodoForm');
if(editForm.todoText.value !== ""){
const data = new FormData();
data.append('id', editForm.id.value);
data.append('todoText', editForm.todoText.value);
data.append('todoDesc', editForm.todoDesc.value);
this.props.mappedEditTodo(data);
}
else{
return;
}
}hideDeleteModal(){
this.props.mappedhideDeleteModal();
}
showDeleteModal(todoToDelete){
this.props.mappedshowDeleteModal(todoToDelete);
}
cofirmDeleteTodo(){
this.props.mappedDeleteTodo(this.props.mappedTodoState.todoToDelete);
}render(){
const todoState = this.props.mappedTodoState;
const todos = todoState.todos;
const editTodo = todoState.todoToEdit;
return(
<div className="col-md-12">
<h3 className="centerAlign">Todos</h3>
{!todos && todoState.isFetching &&
<p>Loading todos....</p>
}
{todos.length <= 0 && !todoState.isFetching &&
<p>No Todos Available. Add A Todo to List here.</p>
}
{todos && todos.length > 0 && !todoState.isFetching &&
<table className="table booksTable">
<thead>
<tr><th>Todo</th><th className="textCenter">Edit</th><th className="textCenter">Delete</th><th className="textCenter">View</th></tr>
</thead>
<tbody>
{todos.map((todo,i) => <tr key={i}>
<td>{todo.todoText}</td>
<td className="textCenter"><Button onClick={() => this.showEditModal(todo)} bsStyle="info" bsSize="xsmall"><Glyphicon glyph="pencil" /></Button></td>
<td className="textCenter"><Button onClick={() => this.showDeleteModal(todo)} bsStyle="danger" bsSize="xsmall"><Glyphicon glyph="trash" /></Button></td>
<td className="textCenter"><Link to={`/${todo._id}`}>View Details</Link> </td>
</tr> )
}
</tbody>
</table>
}
{/* Modal for editing todo */}
<Modal
show={todoState.showEditModal}
onHide={this.hideEditModal}
container={this}
aria-labelledby="contained-modal-title"
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title">Edit Your Todo</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="col-md-12">
{editTodo &&
<TodoEditForm todoData={editTodo} editTodo={this.submitEditTodo} />
}
{editTodo && todoState.isFetching &&
<Alert bsStyle="info">
<strong>Updating...... </strong>
</Alert>
}
{editTodo && !todoState.isFetching && todoState.error &&
<Alert bsStyle="danger">
<strong>Failed. {todoState.error} </strong>
</Alert>
}
{editTodo && !todoState.isFetching && todoState.successMsg &&
<Alert bsStyle="success">
Book <strong> {editTodo.todoText} </strong>{todoState.successMsg}
</Alert>
}
</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.hideEditModal}>Close</Button>
</Modal.Footer>
</Modal>
{/* Modal for deleting todo */}
<Modal
show={todoState.showDeleteModal}
onHide={this.hideDeleteModal}
container={this}
aria-labelledby="contained-modal-title"
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title">Delete Your Book</Modal.Title>
</Modal.Header>
<Modal.Body>
{todoState.todoToDelete && !todoState.error && !todoState.isFetching &&
<Alert bsStyle="warning">
Are you sure you want to delete this todo <strong>{todoState.todoToDelete.todoText} </strong> ?
</Alert>
}
{todoState.todoToDelete && todoState.error &&
<Alert bsStyle="warning">
Failed. <strong>{todoState.error} </strong>
</Alert>
}
{todoState.todoToDelete && !todoState.error && todoState.isFetching &&
<Alert bsStyle="success">
<strong>Deleting.... </strong>
</Alert>
}
{!todoState.todoToDelete && !todoState.error && !todoState.isFetching&&
<Alert bsStyle="success">
Todo <strong>{todoState.successMsg} </strong>
</Alert>
}
</Modal.Body>
<Modal.Footer>
{!todoState.successMsg && !todoState.isFetching &&
<div>
<Button onClick={this.cofirmDeleteTodo}>Yes</Button>
<Button onClick={this.hideDeleteModal}>No</Button>
</div>
}
{todoState.successMsg && !todoState.isFetching &&
<Button onClick={this.hideDeleteModal}>Close</Button>
}
</Modal.Footer>
</Modal>

</div>
);
}
}

Now Start your app using npm start, if you have stopped. Try Deleting Todo, As soon as you confirm the deletion of a Todo it will be removed from your Todo List and permanently deleted from your mongoDB database.

Glad to we got to the end of this tutorial, hopefully it’s enough to get you started and understanding how these technologies all tie together. Make sure your express server and mongoDB instance both are running on their terminals.

// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
fetchTodos: () => dispatch(todoActions.fetchTodos()),
mappedDeleteTodo: todoToDelete => dispatch(todoActions.deleteTodo(todoToDelete)),
mappedEditTodo: todoToEdit => dispatch(todoActions.editTodo(todoToEdit)),
mappedshowEditModal: todoToEdit => dispatch(todoActions.showEditModal(todoToEdit)),
mappedhideEditModal: () => dispatch(todoActions.hideEditModal()),
mappedshowDeleteModal: todoToDelete => dispatch(todoActions.showDeleteModal(todoToDelete)),
mappedhideDeleteModal: () => dispatch(todoActions.hideDeleteModal())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Todos);

--

--