DEV Community

Sirwan Afifi
Sirwan Afifi

Posted on • Updated on

Server-Sent Events and React

Originally posted on my blog

Server Sents Events are realtime events sent from the server to the client. It's a way to subscribe to a data stream sent by a server. Basically, it is a long-running HTTP connection with a particular mime type. Contrary to WebSocket, Server-Sent Events are unidirectional which clients subscribe to a channel and get data from the server. Updates for the data are pushed to the client in realtime as they occur, so the client doesn't need to initiate any requests. When using SSE, we should consider these:

  • Requests can be redirected HTTP 301(permanent) & 307(temporary)
  • Only UTF-8 decoding is supported, no binary data
  • Protocol supports multiple type of events, default is message
  • Clients always reconnect (no need to handle)
  • Server sends HTTP 204 No Content to stop reconnection
  • Limited amount of global connections per site

Server Implementation:

Implementing it on the server is pretty easy. All we need to do is running an endless loop. Inside the loop, we should set the somespecial HTTP headers and push the data to the response every 2 seconds:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
        ILogger<Startup> loggerDebug, Datasource datasource)
{
    app.UseCors("MyPolicy");

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/stream", async context =>
        {
            var response = context.Response;
            response.Headers.Add("connection", "keep-alive");
            response.Headers.Add("cach-control", "no-cache");
            response.Headers.Add("content-type", "text/event-stream");

            while (true)
            {
                await response.Body
                    .WriteAsync(Encoding.UTF8.GetBytes($"data: {JsonSerializer.Serialize(datasource.GetData())}\n\n"));

                await response.Body.FlushAsync();
                await Task.Delay(2 * 1000);
            }

        });
    });
}
Enter fullscreen mode Exit fullscreen mode

Here's also a Node.js version of the server:

app.get("/stream", (req, res) => {
  res.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",

    // enabling CORS
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers":
      "Origin, X-Requested-With, Content-Type, Accept",
  })

  setInterval(() => {
    res.write(`data: ${JSON.stringify(getData())}\n\n`)
  }, 2000)
})
Enter fullscreen mode Exit fullscreen mode

As you can see, we are sending the data in a specific format:

data: My message\n\n
Enter fullscreen mode Exit fullscreen mode

Frontend Implementation

Implementing it on the client is fairly easy. All we need to do is making use of EventSource API, which is a standard interface to interact with the Server-Sent Events protocol. It basically opens a persistent connection to our HTTP server. The cool thing about the API is that it keeps the connection open until we call EventSource.close():

import React from "react"

const ProductList = () => {
  const [data, setData] = React.useState([] as any)

  const formatter = new Intl.NumberFormat("en-GB", {
    style: "currency",
    currency: "gbp",
  })

  React.useEffect(() => {
    let eventSource = new EventSource("http://localhost:5000/stream")
    eventSource.onmessage = e => updateProdutList(JSON.parse(e.data))
  }, [])

  const updateProdutList = (product: any) => {
    setData([...product])
  }

  return (
    <table className="table table-hover">
      <thead className="thead-dark">
        <tr>
          <th>Id</th>
          <th>Title</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
        {data.map((p: any) => (
          <tr key={p.Id}>
            <td>{p.Id}</td>
            <td>{p.Title}</td>
            <td>{formatter.format(p.Price)}</td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}

export { ProductList }
Enter fullscreen mode Exit fullscreen mode

Here we have passed in the SSE endpoint URL to the EventSource constructor. It then goes and establishes the communication channel between the React app and the server. Then we added onmessage event handler which is called when new data is received. updateProdutList is responsible for updating the state so once the data is received we update the state with the latest data. The process of subscribing to the endpoint happens once the ProductList component is mounted; that's why we used useEffect inside the component.

You can see the output here
Source code (https://github.com/SirwanAfifi/realtime)

Top comments (3)

Collapse
 
billyc10 profile image
billyc10 • Edited

I noticed that in the Node.js implementation, even if the client closes the connection, the setInterval function will continue running. I made an edit so it handles connection closures:


let eventInterval = setInterval(() => {
    res.write(`data: ${JSON.stringify(getData())}\n\n`)
  }, 2000)

req.on('close', (err) => {
    clearInterval(eventInterval);
    res.end();
    })
Collapse
 
sirwanafifi profile image
Sirwan Afifi

Perfect, thanks, I updated the source code on GitHub

Collapse
 
christopherkapic profile image
Christopher Kapic

Should you use a cleanup function in the useEffect so that when the component unmounts you are no longer subscribed to the EventSource?