Sending messages to clients in real-time using Server-sent Events, NodeJS and React
Tags: node, react, javascript, realtime
Published: 2022-05-23 15:52:46

## Overview
Server-Sent Events(SSE) technology allows sending data from server to clients in real-time, it's based on HTTP.

On the client side server-sent events provides `EventSource` API (part of the HTML5 standard), that allows us to open a permanent connection to the HTTP server and receive messages(events) from it.

On the server side headers are required to keep the connection open. The `Content-Type` header set to `text/event-stream` and the `Connection` header set to `keep-alive`.

The connection remains open until closed by calling `EventSource.close()`.

Restrictions:
- Allows only receive events from server (unidirectional data flow, unlike WebSockets);
- Events are limited to `UTF-8` (no binary data).

Possible benefits:
- Because SSE works via `HTTP`, it will be work on clients that use proxy, that not support other protocols (like WebSocket);
- If connection use `HTTPS` then no need to think about traffic encryption.

Browsers support: [https://caniuse.com/eventsource](https://caniuse.com/eventsource).

In this article we will develop `Todo List` app, that allow us add, delete, mark as done tasks in the list.

The state of the Todo List will be shared among all connected users via Server-Sent Events.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ukny11a5lp4l2fx52lpm.gif)

## Step 1 - Building Express Backend

```console
# Create and go to project directory
mkdir sse
cd sse

# Create and go to subdirectory of backend part of project
mkdir server
cd server

# Initialize project and install required dependencies
npm init -y
npm install express@^4.18.1 body-parser@^1.20.0 compression@^1.7.4 cors@^2.8.5 --save
```

After installing required dependencies open `package.json` and add `"type": "module"` after project name. This is necessary so that NodeJS can work with javascript modules.
```json
{
   "name": "server",
   "type": "module"
   ...
}
```
Create file `server.js` and add some template code:
```javascript
import express from 'express';
import compression from 'compression';
import bodyParser from 'body-parser';
import cors from 'cors';

const app = express();
app.use(compression());
app.use(cors());
app.use(bodyParser.json());

let clients = [];
let todoState = [];

app.get('/state', (req, res) => {
    res.json(todoState);
});

const PORT = process.env.PORT || 3005;
app.listen(PORT, () => {
   console.log(`Shared todo list server listening at http://localhost:${PORT}`);
});
```
Starting the server with the command `npm start`. If everything is done correctly, then by making the request `curl http://localhost:3005/state` you will see `[]` - an empty list of todo sheet.
Next, before the port declaration `const PORT = process.env.PART || 3005;` add the code to connect the client via SSE:
```javascript
app.get('/events', (req, res) => {
   const headers = {
      // The 'text/event-stream' connection type
      // is required for SSE
      'Content-Type': 'text/event-stream',
      'Access-Control-Allow-Origin': '*',
      // Setting the connection open 'keep-alive'
      'Connection': 'keep-alive',
      'Cache-Control': 'no-cache'
   };
   // Write successful response status 200 in the header
   res.writeHead(200, headers);
   
   /*
   Data Shaping:
   When the EventSource receives multiple consecutive
   lines that begin with data:, it concatenates them,
   inserting a newline character between each one.
   Trailing newlines are removed.
   Double trailing newline \n\n is mandatory to indicate
   the end of an event
   */
   const sendData = `data: ${JSON.stringify(todoState)}\n\n`;
   
   res.write(sendData);
   // If compression middleware is used, then res.flash()
   // must be added to send data to the user
   res.flush();
   
   // Creating a unique client ID
   const clientId = genUniqId();
   const newClient = {
      id: clientId,
      res,
   };
   
   clients.push(newClient);
   
   console.log(`${clientId} - Connection opened`);

   req.on('close', () => {
      console.log(`${clientId} - Connection closed`);
      clients = clients.filter(client => client.id !== clientId);
   });
});

function genUniqId(){
   return Date.now() + '-' + Math.floor(Math.random() * 1000000000);
}
```
So, we wrote the code that allows the client to connect by establishing a permanent connection, and also saved the `id` and `res` in the array of clients so that in the future we could send data to connected clients.

To check that everything is working, we will add a code to transfer the unique `ids` of the connected users.
```javascript
app.get('/clients', (req, res) => {
   res.json(clients.map((client) => client.id));
});
```
Starting the server `npm start`.
We connect to the server in the new terminal:
```console
curl -H Accept:text/event-stream http://localhost:3005/events
```
In different terminals, you can repeat the command several times to simulate the connection of several clients.
Checking the list of connected:
```console
curl http://localhost:3005/clients
```
In the terminal, you should see an array of `ids` of connected clients:
```console
["1652948725022-121572961","1652948939397-946425533"]
```

Now let's start writing the business logic of the Todo List application, we need:
**a)** Add a task to the todo list;
**b)** Delete a task from the todo list;
**c)** Set/unset task completion;
**d)** After each action, send the state to all connected clients.

The todo list state will look like this:
```javascript
[
   {
      id: "1652980545287-628967479",
      text: "Task 1",
      checked: true
   },
   {
      id: "1652980542043-2529066",
      text: "Task 2",
      checked: false
   },
   ...
]
```
Where `id` is a unique identifier generated by the server, `text` is the text of the task, `checked` is the state of the task checkbox.

Let's start with **d)** - after each action, send the state to all connected clients:
```javascript
function sendToAllUsers() {
   for(let i=0; i<clients.length; i++){
      clients[i].res.write(`data: ${JSON.stringify(todoState)}\n\n`);
      clients[i].res.flush();
   }
}
```
Then we implement **a)** **b)** and **c)**:
```javascript
// Add a new task to the list and
// send the state to all clients
app.post('/add-task', (req, res) => {
   const addedText = req.body.text;
   todoState = [
      { id: genUniqId(), text: addedText, checked: false },
      ...todoState
   ];
   res.json(null);
   sendToAllUsers();
});

// Change the state of the task in the list
// and send the result state to all clients
app.post('/check-task', (req, res) => {
   const id = req.body.id;
   const checked = req.body.checked;
   todoState = todoState.map((item) => {
      if(item.id === id){
         return { ...item, checked };
      }
      else{
         return item;
      }
   });
   res.json(null);
   sendToAllUsers();
});

// Remove the task from the list and
// send the new state of the list to all clients
app.post('/del-task', (req, res) => {
   const id = req.body.id;
   todoState = todoState.filter((item) => {
      return item.id !== id;
   });
   res.json(null);
   sendToAllUsers();
});
```
So, the server part is ready. Full code of the server:
```js
import express from 'express';
import compression from 'compression';
import bodyParser from 'body-parser';
import cors from 'cors';

const app = express();
app.use(compression());
app.use(cors());
app.use(bodyParser.json());

let clients = [];
let todoState = [];

app.get('/state', (req, res) => {
   res.json(todoState);
});

app.get('/events', (req, res) => {
   const headers = {
      'Content-Type': 'text/event-stream',
      'Access-Control-Allow-Origin': '*',
      'Connection': 'keep-alive',
      'Cache-Control': 'no-cache'
   };
   res.writeHead(200, headers);
   
   const sendData = `data: ${JSON.stringify(todoState)}\n\n`;
   res.write(sendData);
   res.flush();
   
   const clientId = genUniqId();

   const newClient = {
      id: clientId,
      res,
   };
   
   clients.push(newClient);

   console.log(`${clientId} - Connection opened`);

   req.on('close', () => {
      console.log(`${clientId} - Connection closed`);
      clients = clients.filter(client => client.id !== clientId);
   });
});

function genUniqId(){
   return Date.now() + '-' + Math.floor(Math.random() * 1000000000);
}

function sendToAllUsers() {
   for(let i=0; i<clients.length; i++){
      clients[i].res.write(`data: ${JSON.stringify(todoState)}\n\n`);
      clients[i].res.flush();
   }
}

app.get('/clients', (req, res) => {
   res.json(clients.map((client) => client.id));
});

app.post('/add-task', (req, res) => {
   const addedText = req.body.text;
   todoState = [
      { id: genUniqId(), text: addedText, checked: false },
      ...todoState
   ];
   res.json(null);
   sendToAllUsers();
});

app.post('/check-task', (req, res) => {
   const id = req.body.id;
   const checked = req.body.checked;
   
   todoState = todoState.map((item) => {
      if(item.id === id){
         return { ...item, checked };
      }
      else{
         return item;
      }
   });
   res.json(null);
   sendToAllUsers();
});

app.post('/del-task', (req, res) => {
   const id = req.body.id;
   todoState = todoState.filter((item) => {
      return item.id !== id;
   });

   res.json(null);
   sendToAllUsers();
});

const PORT = process.env.PORT || 3005;
app.listen(PORT, () => {
    console.log(`Shared todo list server listening at http://localhost:${PORT}`);
});
```
Then proceed to the second step - the client part.

## Step 2 - Building the Client part: React application

Go to the previously created project folder `sse`, then run the command to create the react application template:
```console
npx create-react-app client
```
Next, go to the folder of the created application and launch it:
```console
cd client
npm start
```
After that, the client application page should open in the browser [http://localhost:3000](http://localhost:3000).

Next, go to the file `src/index.js ` and remove `React.StrictMode` from the application.
```javascript
// Before
root.render(
   <React.StrictMode>
      <App />
   </React.StrictMode>
);
// After
root.render(
   <App />
);
```
The fact is that `React StrictMode` renders components twice in `development` mode to detect possible problems. But in our case, this is not necessary, otherwise the client will connect to the server twice and establish a permanent connection.

Remove all the contents from the `App.css` file and insert our own styles:
```css
h1 {
   text-align: center;
}
main {
   display: flex;
   justify-content: center;
}
.l-todo {
   max-width: 31.25rem;
}
form {
   margin-bottom: 1rem;
}
form input[type="submit"] {
    margin-left: 0.5rem;
}
.task-group {
   margin-bottom: 0.125rem;
   display: flex;
   flex-wrap: nowrap;
   justify-content: space-between;
}
.task-group button {
   padding: 0.25rem 0.5rem;
   margin-left: 0.5rem;
   border: none;
   background-color: white;
}
```
Let's prepare the application template, remove from the `App.js` all content and insert our:
```jsx
import './App.css';
import { useState, useEffect, useRef } from 'react';

function App(){
   return(
      <main>
      </main>
   );
}

export default App;
```
Let's add a list state hook to our `App` component:
```jsx
const [tasks, setTasks] = useState([]);
```
Now let's add the `useEffect` hook in which we will establish a permanent SSE connection:
```jsx
useEffect(() => {
   let mount = true;
   let events;
   let timer;
   let createEvents = () => {
      // Close connection if open
      if(events){
            events.close();
      }
      // Establishing an SSE connection
      events = new EventSource(`http://localhost:3005/events`);
      events.onmessage = (event) => {
            // If the component is mounted, we set the state
            // of the list with the received data
            if(mount){
               let parsedData = JSON.parse(event.data);
               setTasks(parsedData);
            }
      };
      // If an error occurs, we wait a second
      // and call the connection function again
      events.onerror = (err) => {
            timer = setTimeout(() => {
               createEvents();
            }, 1000);
      };
   };
   createEvents();

   // Before unmounting the component, we clean
   // the timer and close the connection
   return () => {
      mount = false;
      clearTimeout(timer);
      events.close();
   }
}, []);
```
Now when opening the client site [http://localhost:3000](http://localhost:3000) a connection to the server will occur and the server will send the todo list state to the connected client. The client, after receiving the data, will set the state of the todo list.

Let's develop an interface component for adding a new task to the list.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gm0kn5mp5jwuwfqgqjto.png)
Add a file to the project `src/AddTask.js `
```jsx
function AddTask(props){
   const { text, onTextChange, onSubmit, textRef } = props;
   return(
      <form onSubmit={onSubmit}>
         <input
            type="text"
            name="add"
            value={text}
            onChange={onTextChange}
            ref={textRef}
         />
         <input
            type="submit"
            value="Add"
         />
      </form>
   );
}

export default AddTask;
```
Creating a list item element:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2gm9aa0ezlskpvyajx0t.png)
Add a file to the project `src/Task.js `:
```jsx
function Task(props){
   const { id, text, checked, onCheck, onDel } = props;
   return(
      <div className="task-group">
         <div>
            <input
               type="checkbox"
               name={`chk${id}`}
               id={`chk${id}`}
               checked={checked}
               onChange={onCheck}
            />
            <label htmlFor={`chk${id}`}>{text}</label>
         </div>
         <button
            id={`btn${id}`}
            onClick={onDel}>x
         </button>
      </div>
   );
}

export default Task;
```
Include the created files to `App.js `:
```jsx
import AddTask from './AddTask';
import Task from './Task';
```
In our application, we will transmit data to the server in `JSON` format, so before moving on, we will write a small wrapper for the javascript fetch API to simplify the client code. Create a file `/src/jsonFetch.js `:
```js
function jsonFetch(url, data){
   return new Promise(function(resolve, reject){
      fetch(url, {
         method: 'POST',
         headers: {
               'Content-Type': 'application/json'
         },
         body: JSON.stringify(data)
      })
      .then(function(res){
         if(res.ok){
               const contentType = res.headers.get('content-type');
               if(contentType && contentType.includes('application/json')){
                  return res.json();
               }
               return reject(`Not JSON, content-type: ${contentType}`);
         }
         return reject(`Status: ${res.status}`);
      })
      .then(function(res){
         resolve(res);
      })
      .catch(function(error){
         reject(error);
      });
   });
}

export default jsonFetch;
```
Include created file in `App.js`:
```jsx
import jsonFetch from './jsonFetch';
```
Now let's add our `addTask` and `Task` components to the `App` component:
```jsx
function App(){
   const [addTaskText, setAddTaskText] = useState('');
   const [tasks, setTasks] = useState([]);
   const addTextRef = useRef(null);
   
   useEffect(() => {
   // SSE code
      ...
   },[]);
   
   const tasksElements = tasks.map((item) => {
      return(
         <Task
            key={item.id}
            id={item.id}
            text={item.text}
            checked={item.checked}
            onCheck={handleTaskCheck}
            onDel={handleTaskDel}
         />
      );
   });

   return (
      <main>
         <div className="l-todo">
               <h1>Todo List</h1>
               <AddTask
                  text={addTaskText}
                  onSubmit={handleAddTaskSubmit}
                  onTextChange={handleAddTaskTextChange}
                  textRef={addTextRef}
               />
               {tasksElements}
         </div>
      </main>
   );
}
```
Let's write user event handlers in the `App` component:
```jsx
function handleAddTaskTextChange(event){
   setAddTaskText(event.target.value);
}

function handleAddTaskSubmit(event){
   event.preventDefault();
   let addedText = addTaskText.trim();
   if(!addedText){
      return setAddTaskText('');
   }
   jsonFetch('http://localhost:3005/add-task', {text: addedText})
   .then(() => {
      setAddTaskText('');
   })
   .catch((err) => {
      console.log(err);
   })
   .finally(() => {
      addTextRef.current.focus();
   });
}

function handleTaskCheck(event){
   const checked =  event.target.checked;
   const targetId = event.target.id.substring(3);
        
   jsonFetch('http://localhost:3005/check-task', {id: targetId, checked})
   .catch((err) => {
      console.log(err);
   });
}

function handleTaskDel(event){
   let targetId = event.target.id.substring(3);
        
   jsonFetch('http://localhost:3005/del-task', {id: targetId})
   .catch((err) => {
      console.log(err);
   });
}
```
So, the logic of the client application: when component did mount, a SSE connection is created to the server, which transmits the state of the list when connected. After receiving the state of the list from the server, it is set to the client `setTasks(parsedData)`.
Further, when adding, deleting and set/unset tasks, the changes are sending to the server, there they are recording to `todoState` and transmitting to all connected users.

Full client application code:
```jsx
import './App.css';
import { useState, useEffect, useRef } from 'react';
import AddTask from './AddTask';
import Task from './Task';
import jsonFetch from './jsonFetch';

function App(){
   const [addTaskText, setAddTaskText] = useState('');
   const [tasks, setTasks] = useState([]);
   const addTextRef = useRef(null);

   useEffect(() => {
      let mount = true;
      let events;
      let timer;
      let createEvents = () => {
         if(events){
            events.close();
         }
         events = new EventSource(`http://localhost:3005/events`);
         events.onmessage = (event) => {
            if(mount){
               let parsedData = JSON.parse(event.data);
               setTasks(parsedData);
            }
         };
         events.onerror = (err) => {
            timer = setTimeout(() => {
               createEvents();
            }, 1000);
         };
      };
      createEvents();

      return () => {
         mount = false;
         clearTimeout(timer);
         events.close();
      }
   }, []);

   const tasksElements = tasks.map((item) => {
      return(
         <Task
            key={item.id}
            id={item.id}
            text={item.text}
            checked={item.checked}
            onCheck={handleTaskCheck}
            onDel={handleTaskDel}
         />
      );
   });

   return (
      <main>
         <div className="l-todo">
               <h1>Todo List</h1>
               <AddTask
                  text={addTaskText}
                  onSubmit={handleAddTaskSubmit}
                  onTextChange={handleAddTaskTextChange}
                  textRef={addTextRef}
               />
               {tasksElements}
         </div>
      </main>
   );

   function handleAddTaskTextChange(event){
      setAddTaskText(event.target.value);
   }

   function handleAddTaskSubmit(event){
      event.preventDefault();
      let addedText = addTaskText.trim();
      if(!addedText){
         return setAddTaskText('');
      }
      jsonFetch('http://localhost:3005/add-task', {text: addedText})
      .then(() => {
         setAddTaskText('');
      })
      .catch((err) => {
         console.log(err);
      })
      .finally(() => {
         addTextRef.current.focus();
      });
   }

   function handleTaskCheck(event){
      const checked =  event.target.checked;
      const targetId = event.target.id.substring(3);
      
      jsonFetch('http://localhost:3005/check-task', {id: targetId, checked})
      .catch((err) => {
         console.log(err);
      });
   }

   function handleTaskDel(event){
      let targetId = event.target.id.substring(3);
      
      jsonFetch('http://localhost:3005/del-task', {id: targetId})
      .catch((err) => {
         console.log(err);
      });
   }
}

export default App;
```

Please support me, like and write comments.