Socket.io in Mern Todo App

Bipin Swarnkar
17 min readOct 18, 2017

Handle Real-time Todos using socket.io.

Complete Source Code for this App:

In this tutorial we are going to add socket.io in our Mern (MongoDB, Express, ReactJs, Node.js) Todo App tutorial for the realtime communications. Here we are not going to build new app, we will clone our already available Mern App hosted on Github repo:

So, it is highly recommend to read the Mern tutorial first:

and then continue to this tutorial.

One of hottest topic in node.js is building realtime web applications and Socket.io, A realtime library for Node.js, is very popular to handle real-time data.

First of all clone the Mern app from Github:

git clone https://github.com/bipinswarnkar1989/mern_stack_tutorial_todo_app.git

Make sure you are in the mean_stack_totorial_todo_app directory

cd mern_stack_tutorial_todo_app

Open the server repository in your current terminal and install the server dependencies:

cd express-server && npm install

Install socket.io in server:

npm install socket.io --save

Then in a new terminal Open the react frontend repository and install the reactjs dependencies:

cd react-redux-client && npm install

Install socket.io in react client:

npm install socket.io --save

Then run your express server and react frontend in their terminals by running in each of these (don’t forget to run MongoDb instance first):

npm start

Our cloned app is running now will add socket in our three major functionalities Add Todo, Edit Todo and Delete Todo one by one . But first of all we need to know what socket.io is and its working flow.

Socket.IO is a JavaScript library for real-time web applications. It enables bidirectional communication between web clients and server. Both sides have identical API and are event-driven like Node.js. To open this interactive communication session between the browser and a server, we have to create an HTTP server to enable real-time communication. It will allow us to emit and receive messages. The socket is the object that handles this communication between web clients and server.

Great we just discussed about the Socket.IO , Let’s start adding socket to our Mern App.

Open up main express server file app.js and revise, add following code for socket connection and to receive Todo from client:

./express-server/app.js

// ./express-server/app.js
import express from 'express';
import path from 'path';
import bodyParser from 'body-parser';
import logger from 'morgan';
import mongoose from 'mongoose';
import SourceMapSupport from 'source-map-support';
import bb from 'express-busboy';
import http from 'http';
import socket from 'socket.io';
// import routes
import todoRoutes from './routes/todo.server.route';
//import controller file
import * as todoController from './controllers/todo.server.controller';
// define our app using express
const app = express();
const server = http.Server(app);
const io = socket(server);
// express-busboy to parse multipart/form-data
bb.extend(app);
// socket.io connection
io.on('connection', (socket) => {
console.log("Connected to Socket!!"+ socket.id);
// Receiving Todos from client
socket.on('addTodo', (Todo) => {
console.log('socketData: '+JSON.stringify(Todo));
todoController.addTodo(io,Todo);
});
// Receiving Updated Todo from client
socket.on('updateTodo', (Todo) => {
console.log('socketData: '+JSON.stringify(Todo));
todoController.updateTodo(io,Todo);
});
// Receiving Todo to Delete
socket.on('deleteTodo', (Todo) => {
console.log('socketData: '+JSON.stringify(Todo));
todoController.deleteTodo(io,Todo);
});
})
// allow-cors
app.use(function(req,res,next){
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
})
// configure app
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended:true }));
app.use(express.static(path.join(__dirname, 'public')));
// set the port
const port = process.env.PORT || 3001;
// connect to database
mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost/mern-todo-app', {
useMongoClient: true,
});
// add Source Map Support
SourceMapSupport.install();
app.use('/api', todoRoutes);app.get('/', (req,res) => {
return res.end('Api working');
});
// catch 404
app.use((req, res, next) => {
res.status(404).send('<h2 align=center>Page Not Found!</h2>');
});
// start the server
server.listen(port,() => {
console.log(`App Server Listening at ${port}`);
});

In the above code we imported our controller file todo.server.controller directly in the main file because we are receiving Todo in this main file and skipping the router, forwarding Todo directly to our server controller because router has no role in socket communications, but we need router for getting All Todos and single Todo.

  • io.on(‘connection’, callback) is Used to establish connection to the server.
  • socket.on(message, callback) is Used to receive Todos from the client. callback is invoked on reception.

Revise the express server router file as following:

./express-server/routes/todo.server.route.js

// ./express-server/routes/todo.server.route.js
import express from 'express';
//import controller file
import * as todoController from '../controllers/todo.server.controller';
// get an instance of express router
const router = express.Router();
router.route('/')
.get(todoController.getTodos);
router.route('/:id')
.get(todoController.getTodo);
export default router;

Open up your server controller file todo.server.controller.js, modify addTodo, updateTodo and deleteTodo arrow function.

./express-server/controllers/todo.server.controller.js

// ./express-server/controllers/todo.server.controller.js
import mongoose from 'mongoose';
//import models
import Todo from '../models/todo.server.model';
export const getTodos = (req,res) => {
Todo.find().exec((err,todos) => {
if(err){
return res.json({'success':false,'message':'Some Error'});
}
return res.json({'success':true,'message':'Todos fetched successfully',todos});
});
}
export const addTodo = (io,T) => {
let result;
const newTodo = new Todo(T);
newTodo.save((err,todo) => {
if(err){
result = {'success':false,'message':'Some Error','error':err};
console.log(result);
}
else{
const result = {'success':true,'message':'Todo Added Successfully',todo}
io.emit('TodoAdded', result);
}
})
}
export const updateTodo = (io,T) => {
let result;
Todo.findOneAndUpdate({ _id:T.id }, T, { new:true }, (err,todo) => {
if(err){
result = {'success':false,'message':'Some Error','error':err};
console.log(result);
}
else{
result = {'success':true,'message':'Todo Updated Successfully',todo};
io.emit('TodoUpdated', result);
}
})
}
export const getTodo = (req,res) => {
Todo.find({_id:req.params.id}).exec((err,todo) => {
if(err){
return res.json({'success':false,'message':'Some Error'});
}
if(todo.length){
return res.json({'success':true,'message':'Todo fetched by id successfully',todo});
}
else{
return res.json({'success':false,'message':'Todo with the given id not found'});
}
})
}
export const deleteTodo = (io,T) => {
let result;
Todo.findByIdAndRemove(T.id, (err,todo) => {
if(err){
result = {'success':false,'message':'Some Error','error':err};
console.log(result);
}
result = {'success':true,'message':todo.todoText+'Todo deleted successfully'};
io.emit('TodoDeleted', result);
})
}

We just finished our server side code for socket.io. Let’s move to react client side.

Open your App.js container and map a new action to props.

./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,socket) => dispatch(todoActions.addNewTodo(todo,socket)),
showTodoAddedBySocket: data => dispatch(todoActions.addNewTodoRequestSuccess(data.todo, data.message))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(App);

Add client side code for socket.io in App.js component:

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

// ./react-redux-client/src/components/App.js
import React from 'react';
import { Navbar,Nav,NavItem } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import './App.css';
import TodoForm from './TodoForm';
import io from "socket.io-client";var socket = io.connect('http://localhost:3000');export default class App extends React.Component {
constructor(props){
super(props);
this.toggleAddTodo = this.toggleAddTodo.bind(this);
this.addTodo = this.addTodo.bind(this);
socket.on('TodoAdded', (data) => {
console.log('TodoAdded: '+JSON.stringify(data));
this.props.showTodoAddedBySocket(data);
});
}
toggleAddTodo(e){
e.preventDefault();
this.props.mappedToggleAddTodo();
}
addTodo(e){
e.preventDefault();
const form = document.getElementById('addTodoForm');
if(form.todoText.value !== "" && form.todoDesc.value !== ""){
const socketData = {
todoText: form.todoText.value,
todoDesc: form.todoDesc.value
}
this.props.mappedAddTodo(socketData,socket);

form.reset();
}
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>
);
}
}

We had removed the multipart formdata from addTodo method because it is not required in socket communication. Now we need to remove fetch() from addNewTodo in todoActions.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,socket) => {
return (dispatch) => {
dispatch(addNewTodoRequest(todo));
socket.emit('addTodo', todo);
}
}
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'
}
}

Now open another tab for the to-do app, if you will add a new Todo on client, we fire an event addTodo, since the server is watching this event, you can see your new Todo is added in TodoList collection in both tab todo app.

Update Todo

Let’s move to Update Todo using socket.io. In order to Updating Todo in real time, availability of socket is required in Todos component and we will achieve this using our appState. So open up app.Reducer.js and socket element in initial state.

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

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

Map the appState in Todos Container and socket to some actions:

./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,
mappedAppState: state.appState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
fetchTodos: () => dispatch(todoActions.fetchTodos()),
mappedEditTodo: (todoToEdit,socket) => dispatch(todoActions.editTodo(todoToEdit,socket)),
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()),
mappedEditSuccessBySocket: data => dispatch(todoActions.editTodoSuccess(data.todo,data.message))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Todos);

Send Updated Todo to the Server and Receive Updated Todo in Todos Client Component:

./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);
const socket = this.props.mappedAppState.socket;
socket.on('TodoUpdated', (data) => {
console.log('TodoUpdated: '+JSON.stringify(data));
this.props.mappedEditSuccessBySocket(data);
})

}
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 socketData = {
id:editForm.id.value,
todoText:editForm.todoText.value,
todoDesc:editForm.todoDesc.value
};
this.props.mappedEditTodo(socketData,this.props.mappedAppState.socket);

}
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>
);
}
}

Remove fetch() and add socket from editTodo in todoActions.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,socket) => {
return (dispatch) => {
dispatch(addNewTodoRequest(todo));
socket.emit('addTodo', todo);
}
}
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,socket) => {
return (dispatch) => {
dispatch(editTodoRequest(todo));
socket.emit('updateTodo', todo);
}
}
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'
}
}

At this point in our app, you can Add Todo and Edit your Todo in Real Time. The only thing which is left is Delete Todo in Real Time. Let’s finish this.

Delete Todo

Revise your 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':
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 !== action.todoToDelete._id)
return {
...currentState,
todos:filteredTodos,
todo:null,
isFetching: false,
error: null,
successMsg:action.todoToDelete.todoText+' '+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;
}
}
}
}

Remove fetch() and add socket in deleteTodo in todoActions.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,socket) => {
return (dispatch) => {
dispatch(addNewTodoRequest(todo));
socket.emit('addTodo', todo);
}
}
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,socket) => {
return (dispatch) => {
dispatch(editTodoRequest(todo));
socket.emit('updateTodo', todo);
}
}
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,socket) => {
return (dispatch) => {
dispatch(deleteTodoRequest(todo));
socket.emit('deleteTodo', todo);
}
}
export const deleteTodoRequest = (todo) => {
return {
type:'DELETE_TODO_REQUEST',
todo
}
}
export const deleteTodoSuccess = (data) => {
return {
type:'DELETE_TODO_SUCCESS',
message:data.message,
todoToDelete:data.todo

}
}
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'
}
}

Revise Todos container :

./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,
mappedAppState: state.appState
}
}
// map actions to props
const mapDispatchToProps = (dispatch) => {
return {
//you can now say this.props.mappedAppActions
fetchTodos: () => dispatch(todoActions.fetchTodos()),
mappedEditTodo: (todoToEdit,socket) => dispatch(todoActions.editTodo(todoToEdit,socket)),
mappedshowEditModal: todoToEdit => dispatch(todoActions.showEditModal(todoToEdit)),
mappedhideEditModal: () => dispatch(todoActions.hideEditModal()),
mappedDeleteTodo: (todoToDelete,socket) => dispatch(todoActions.deleteTodo(todoToDelete,socket)),
mappedshowDeleteModal: todoToDelete => dispatch(todoActions.showDeleteModal(todoToDelete)),
mappedhideDeleteModal: () => dispatch(todoActions.hideDeleteModal()),
mappedEditSuccessBySocket: data => dispatch(todoActions.editTodoSuccess(data.todo,data.message)),
mappedDeleteTodoBySocket: data =>dispatch(todoActions.deleteTodoSuccess(data))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Todos);

Send and Receive the Todo to delete:

./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);
const socket = this.props.mappedAppState.socket;
socket.on('TodoUpdated', (data) => {
console.log('TodoUpdated: '+JSON.stringify(data));
this.props.mappedEditSuccessBySocket(data);
});
socket.on('TodoDeleted', (data) => {
console.log('TodoDeleted: '+JSON.stringify(data));
this.props.mappedDeleteTodoBySocket(data);
})
}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 socketData = {
id:editForm.id.value,
todoText:editForm.todoText.value,
todoDesc:editForm.todoDesc.value
};
this.props.mappedEditTodo(socketData,this.props.mappedAppState.socket);
}
else{
return;
}
}hideDeleteModal(){
this.props.mappedhideDeleteModal();
}
showDeleteModal(todoToDelete){
this.props.mappedshowDeleteModal(todoToDelete);
}
cofirmDeleteTodo(){
this.props.mappedDeleteTodo(this.props.mappedTodoState.todoToDelete,this.props.mappedAppState.socket);
}
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">
<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>
);
}
}

Finally we finished all three major functionalities Add Todo, Edit Todo and Delete Todo in real time using socket.io.

Hope you enjoyed. Try these three major functionalities in different tabs or different browser and feel free to modify the code as per your need.

--

--