Have you ever tried building a dashboard that updates in real time? Or maybe wondered how sites like ChatGPT stream response data to the page? Or how Figma makes collaborative editing seem so seamless?
Today we'll try to answer these questions by looking at some methods to stream real-time data and build our own small real-time stock market feed app in to display randomly generated data.
Short Polling
The first method we'll take a look at is short polling, which is a technique for somewhat real-time communication.
It involves the client querying the server at regular intervals, usually a few seconds between requests, then updating the data on the client if necessary.
While this isn't exactly "real time", it can be good enough for many scenarios where updates don't need to be immediate or are infrequent, like checking for updates in an email inbox or a weather app where updates can occur every few minutes.
Short polling offers a few advantages:
- It's pretty simple to implement
- It doesn't require a persistent connection
- It works with any HTTP client or browser
So let's take a look at the code:
Server
const updateStockPrices = () => {
stocks = stocks.map((stock) => ({
...stock,
// Randomly increment or decrement the stock price
price: Math.abs(stock.price + getStockPriceChange()),
}));
};
setInterval(updateStockPrices, 5 * 1000);
app.get("/stocks", (c) => {
return c.json({ stocks });
});
Client
const { data: stocksData } = useQuery({
queryKey: ["stocks"],
queryFn: async () =>
fetch("http://localhost:3000/stocks").then((response) =>
response.json() as Promise<{ stocks: Stock[] }>
),
refetchInterval: 5 * 1000,
});
For the server, I simulated frequent data updates by updating stock prices randomly. For simplicity's sake I set an interval to update the data every 5 seconds (even though data updates in real-time apps can be much more unpredictable). As for the client, I used
While simple, this solution has a few issues:
- First of all, in this type of app the data can change quite frequently. This means that the interval might be too low and the client might "miss" some data on the server. While we can always lower the fetching interval, this can come at the cost of load to our server and additional network traffic on the client.
- Many times, we might be querying the server when the data hasn't really changed, creating even more unnecessary load on the server.
- While fetching the data often lowers the margin of error we might encounter on the client, we still don't get a true real time experience.
WebSockets
The next method we'll look at is WebSockets. Using WebSockets allows a persistent, bi-directional (two-way) communication channel between a client and a server. Unlike traditional HTTP, which follows a request-response model, WebSockets keep the connection open, so both the client and server can send messages to each other at any time.
WebSocket connections can be created over HTTP (ws) or HTTPS (wss), with wss ensuring the communication is encrypted and secure.
How it works
Handshake:
The connection starts with an HTTP handshake, after which the protocol ) or live updating dashboards (like the one we're trying to build!).
WebSockets also offer a few advantages:
They offer true real-time communication, unlike short polling, making them ideal for high-frequency low latency updates.
Network efficiency: In that the connection between client and server remains open, reducing repeated handshakes and network overhead.
Bi-directional updates: Both client and server can message each other.
Now let's implement them in our app:
Server
CODEconst updateStockPrices = () => {
stocks = stocks.map((stock) => ({
...stock,
price: stock.price + getStockPriceChange(),
time: Date.now(),
}));
eventEmitter.emit("stocksUpdated");
scheduleNextUpdate();
};
app.get(
"/stocks-ws",
upgradeWebSocket((c) => {
let sendEventsToClient = () => {};
return {
onOpen(_, ws) {
sendEventsToClient = () => {
ws.send(JSON.stringify({ stocks }));
};
eventEmitter.on("stocksUpdated", sendEventsToClient);
ws.send(JSON.stringify({ stocks }));
},
onClose: () => {
eventEmitter.off("stocksUpdated", sendEventsToClient);
},
};
}),
);
Client
// useWebsocket.ts
CODEimport { useEffect, useState } from 'react';
const sockets = new Map<string, { ws: WebSocket; refCount: number }>();
export function useWebsocket<T>(url: string, valueParser: (value: string) => T = JSON.parse) {
const [isReady, setIsReady] = useState(false);
const [value, setValue] = useState<T | null>(null);
useEffect(() => {
if (!sockets.has(url)) {
const ws = new WebSocket(url);
sockets.set(url, { ws, refCount: 0 });
ws.onopen = () => setIsReady(true);
ws.onmessage = event => setValue(valueParser(event.data));
ws.onclose = () => {
sockets.delete(url);
};
}
const socket = sockets.get(url);
if (!socket) return;
socket.refCount += 1;
return () => {
socket.refCount -= 1;
// Close the WebSocket if no more references exist
if (socket.refCount === 0 && socket.ws.OPEN) {
socket.ws.close();
sockets.delete(url);
}
};
}, [url]);
const send = (data: string | ArrayBuffer | SharedArrayBuffer | Blob | ArrayBufferView) => {
const socket = sockets.get(url);
if (socket && socket.ws.readyState === WebSocket.OPEN) {
socket.ws.send(data);
}
};
return { isReady, value, send };
}
// App.tsx
CODEconst { isReady, value: stocksData } = useWebsocket<{ stocks: Stock[] }>('ws://localhost:3000/stocks-ws');
if (!isReady) return;
// render logic
For the server I used to update my endpoint when the data changed. For the client I created my own custom hook, which used the
It's also worth noting that WebSockets can also integrate into your app's existing TanStack Query setup by sending messages of updates and not data, while TanStack Query handles cache and data revalidation, as can be seen in for a relatively lightweight react implementation or on running WebSockets over HTTP/2, it lacks wide support with many client libraries.
Resource consumption: WebSocket connections remain open for their duration, consuming server resources such as memory and CPU. High numbers of concurrent connections can be intensive on server resources.
Security risks: WebSockets bypass many standard HTTP security mechanisms like CORS or CSRF protections, which puts them at risk of .
How it works
- The client initiates an HTTP request, with the
Accept: text/event-streamheader. - The server responds with an
HTTP 200 statusand headers that define the connection is an SSE stream, likeContent-Type: text/event-stream. - The server then sends data over the open connection to the client, which processes the events using the EventSource API.
SSE can be great in scenarios where the server needs to constantly stream real-time data to the client, such as ChatGPT's responses (which use SSE to deliver parts of the response in real time), live notifications (like for updating scores in sports apps), or live-updating dashboards.
So what advantages does SSE have over WebSockets?
Simplicity: SSE can be much simpler than WebSockets, as things like client reconnection are handled automatically by the EventSource API, with no need for additional code or libraries.
Lightweight: SSE is designed for one-way communication (which might be just what we need in our app!). It doesn't require the overhead of managing bidirectional communication, which in turn can lead to lower resource usage.
HTTP/2 compatible: SSE leverages HTTP/2’s multiplexing, allowing multiple streams over a single connection and avoiding the browser’s concurrent connection limits.
Security: SSE integrates with HTTP features like CORS, and works more reliably with firewalls, load balancers, and HTTP security tools compared to WebSockets.
With all that said, let's get to coding!
Server
CODEapp.get("/stocks-sse", async (c) => {
return streamSSE(c, async (stream) => {
const sendEventToClient = async () => {
await stream.writeSSE({ data: JSON.stringify({ stocks }) });
};
eventEmitter.on("stocksUpdated", sendEventToClient);
stream.writeSSE({ data: JSON.stringify({ stocks }) });
stream.onAbort(() => {
eventEmitter.off("message", sendEventToClient);
});
while (true) {
await stream.sleep(200);
}
});
});
Client
// useSSE.ts
CODEimport { useEffect, useState } from 'react';
const eventSources = new Map<string, { es: EventSource; refCount: number }>();
export function useSSE<T>(url: string, valueParser: (value: string) => T = JSON.parse) {
const [isConnected, setIsConnected] = useState(false);
const [value, setValue] = useState<T | null>(null);
const [error, setError] = useState<Event | null>(null);
useEffect(() => {
if (!eventSources.has(url)) {
const es = new EventSource(url);
eventSources.set(url, { es, refCount: 0 });
es.onopen = () => setIsConnected(true);
es.onmessage = event => setValue(valueParser(event.data));
es.onerror = err => {
setError(err);
setIsConnected(false);
// Optionally close the connection on certain errors
if (es.readyState === EventSource.CLOSED) {
eventSources.delete(url);
}
};
}
const eventSource = eventSources.get(url);
if (!eventSource) return;
eventSource.refCount += 1;
return () => {
eventSource.refCount -= 1;
// Close the EventSource if no more references exist
if (eventSource.refCount === 0) {
eventSource.es.close();
eventSources.delete(url);
}
};
}, [url, valueParser]);
return { isConnected, value, error };
}
// App.tsx
CODEconst {isConnected, value: stocksData} = useSSE<{stocks: Stock[]}>("http://localhost:3000/stocks-sse");
if (!isConnected) return;
// render logic
Our server logic is pretty simple thanks to Hono's built in
Are there other methods?
While for this example I feel the methods I covered were the most appropriate, there are other real-time options you can implement in your app.
- If you're looking for streaming between backend microservices, (offer both Websocket and SSE based implementations) and .
Conclusion
We've looked at 3 different methods for real-time updates, all of which have their own unique advantages. For our app, I would say that SSE is probably what I would go for thanks to it providing better real-time updates than short polling and being simpler and more lightweight than WebSockets. That being said, different requirements pose different challenges and there isn't a perfect solution. It's always best to assess the functionality we want to provide and try to find the method that would provide the best user experience and be as simple and maintainable as possible.
If we want to follow a long process that runs on the server in our client, or maybe have notifications that don't need to pop up immediately, I would recommend short polling for its simplicity and for not requiring diving into additional APIs.
In case we do want an immediate response on the client, and only require for the client to receive updates from the server (for things like live updates or real-time notifications), I would recommend SSE.
If we want the bi-directional communication and for our client to be able to send messages to the server, for things like chat apps or collaborative editing, I would recommend WebSockets, and perhaps pairing it with one of the libraries mentioned above.
I hope this article taught you a thing or two about real-time data and how to handle it in your app. If you’ve tried other methods or have ideas from your own projects, I’d love to hear about them in the comments!
You can find the complete code for the project built in this article in . Happy coding!
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.- The client initiates an HTTP request, with the
SOCIAL SHARE CARD GENERATOR