How to Build a Real-Time To-Do App with React and WebSockets

Published on 19.3.2020

Building a real-time to-do app with React is a great way to understand how to manage live updates in a collaborative environment. Whether you are developing a task manager, a collaborative whiteboard, or a simple live notification system, real-time updates add a new layer of interactivity.

In this post, we’ll explore how to use WebSockets in React to create a real-time collaborative to-do list. We’ll walk through:

  • Using WebSockets for real-time communication.
  • Building a collaborative to-do list that updates instantly for all users.
  • Handling optimistic updates to enhance user experience while waiting for the server to respond.

Let’s get started!


What are WebSockets?

WebSockets are a protocol for full-duplex communication channels over a single TCP connection. They allow you to send and receive messages between the client and server instantly, without the need for repeatedly making HTTP requests (as is common with REST APIs).

In a to-do app, WebSockets are particularly useful for ensuring that when one user updates their task list (e.g., adding, editing, or deleting a to-do item), those changes are immediately reflected in the browsers of other users who are connected to the same app.


Setting Up a WebSocket Server

Before we dive into the React client, let’s first set up a WebSocket server. We’ll use Node.js with the ws library to create a simple WebSocket server that broadcasts changes to all connected clients.

1. Install the Required Packages

In your server folder, create a Node.js project and install the necessary dependencies:

npm init -y
npm install ws express

2. Set Up the WebSocket Server

Now, create a simple WebSocket server that will listen for incoming connections and broadcast messages to all connected clients.

// server.js
const express = require('express');
const WebSocket = require('ws');

// Set up Express and WebSocket
const app = express();
const wss = new WebSocket.Server({ noServer: true });

// Broadcast function to send data to all connected clients
const broadcast = (data) => {
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(data));
    }
  });
};

// Handle WebSocket connections
wss.on('connection', (ws) => {
  console.log('A new client connected');

  // Send a welcome message to the new client
  ws.send(JSON.stringify({ message: 'Welcome to the To-Do app!' }));

  // Listen for incoming messages from clients
  ws.on('message', (message) => {
    console.log('Received:', message);
    // Broadcast received data to all connected clients
    broadcast(JSON.parse(message));
  });
});

// Set up server to handle WebSocket connections
app.server = app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
app.server.on('upgrade', (request, socket, head) => {
  wss.handleUpgrade(request, socket, head, (ws) => {
    wss.emit('connection', ws, request);
  });
});

In this setup:

  • We use Express to set up a basic server.
  • We use ws to create a WebSocket server that allows us to communicate with clients.
  • The broadcast function sends messages to all connected clients whenever a change happens.

Setting Up the React Client

Now, let’s move on to building the React front-end that interacts with our WebSocket server.

1. Install WebSocket Client Library

In the React project, install the WebSocket client package:

npm install websocket

2. Create the To-Do App with WebSocket Integration

Create the To-Do App component, which will establish a WebSocket connection and listen for updates.

// App.js
import React, { useState, useEffect } from 'react';
import './App.css';

function App() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');
  const [socket, setSocket] = useState(null);

  // Set up the WebSocket connection
  useEffect(() => {
    const ws = new WebSocket('ws://localhost:3000');
    setSocket(ws);

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.todos) {
        setTodos(data.todos); // Update the todo list when server sends a message
      }
    };

    return () => {
      ws.close(); // Clean up the WebSocket connection on unmount
    };
  }, []);

  // Handle adding a new todo
  const addTodo = () => {
    if (!newTodo) return;
    const updatedTodos = [...todos, { id: Date.now(), task: newTodo }];
    setTodos(updatedTodos);

    // Optimistic update: Update the UI immediately before the server confirms
    socket.send(JSON.stringify({ type: 'add', todo: { id: Date.now(), task: newTodo } }));
    setNewTodo('');
  };

  // Handle deleting a todo
  const deleteTodo = (id) => {
    const updatedTodos = todos.filter(todo => todo.id !== id);
    setTodos(updatedTodos);

    // Optimistic update: Update the UI immediately before the server confirms
    socket.send(JSON.stringify({ type: 'delete', id }));
  };

  return (
    <div className="App">
      <h1>Real-Time To-Do App</h1>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder="Add a new task"
      />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.task}
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

3. Real-Time Updates and Optimistic UI

  • Optimistic Updates: In the addTodo and deleteTodo functions, we update the UI immediately when the user makes changes (before waiting for the server response). This makes the app feel faster and more responsive.
  • WebSocket Communication: When a user adds or deletes a task, we send the corresponding event (either add or delete) to the WebSocket server, which broadcasts it to all connected clients.

Handling Optimistic Updates

Optimistic updates are crucial for providing an instant response to user interactions. In the code above, we handle optimistic updates by immediately updating the local state of the to-do list when the user adds or deletes a task, even before receiving confirmation from the server.

This makes the app feel snappy and improves the user experience.

If you want to manage the states with more sophistication (e.g., rollback in case of failure), you could introduce a pending state that keeps track of the action until confirmation is received.


Conclusion

In this tutorial, we’ve built a real-time collaborative to-do app using React and WebSockets. We explored:

  • How to use WebSockets for real-time updates in React apps.
  • How to build a collaborative to-do list that syncs across all clients in real time.
  • How to implement optimistic updates to make the UI more responsive and improve the user experience.

WebSockets are a powerful tool for real-time apps, and integrating them with React allows you to create seamless, live applications. Whether you’re building a collaborative app or a live notification system, WebSockets offer a simple and efficient solution for instant communication.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *