In this tutorial, I'll show you how to implement Server-Sent Events (SSE) in your frontend application. SSE is a simple and effective way to receive real-time updates from your server without the overhead of WebSockets or the inefficiency of polling.
What are Server-Sent Events?
Server-Sent Events are a standard that allows a web page to get updates from a server. Unlike WebSockets, SSE is a one-way communication channel - the server sends data to the client, but not vice versa. This makes SSE perfect for scenarios like:
- Live feeds and notifications
- Real-time dashboards
- Status updates
- Chat applications (for receiving messages)
- Stock tickers
SSE has several advantages over alternatives:
- Simpler than WebSockets - easier to implement on both client and server
- Automatic reconnection - built into the protocol
- Native browser support - no additional libraries needed
- Regular HTTP - works with standard proxies and firewalls
- Text-based - easy to debug
Browser Support
SSE is supported in all modern browsers including Chrome, Firefox, Safari, and Edge.
Basic Implementation
Let's start with a simple SSE client implementation:
// Create an EventSource instance pointing to the SSE endpoint
const eventSource = new EventSource('/api/events');
// Listen for messages from the server
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received data:', data);
// Update your UI with the received data
};
// Handle connection open
eventSource.onopen = () => {
console.log('SSE connection established');
};
// Handle errors
eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
// Automatic reconnection will happen by default
};
// To close the connection when no longer needed
function closeConnection() {
eventSource.close();
}
Handling Different Event Types
One powerful feature of SSE is the ability to send different types of events over the same connection:
const eventSource = new EventSource('/api/events');
// Listen for specific event types
eventSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
console.log('Received update:', data);
// Handle updates
});
eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
console.log('Received notification:', data);
// Handle notifications
});
// The onmessage handler only catches events without an event type
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received default message:', data);
};
Adding Authentication
For authenticated endpoints, you'll need to include credentials:
// With credentials for authenticated endpoints
const eventSource = new EventSource('/api/events', { withCredentials: true });
If you need to send a token, you might need to customize the request further. Since EventSource doesn't support custom headers directly, you can pass the token in the URL:
const token = 'your-auth-token';
const eventSource = new EventSource(`/api/events?token=${token}`);
Handling Reconnection Explicitly
While SSE has automatic reconnection, sometimes you want more control:
let eventSource;
let retryCount = 0;
const maxRetries = 5;
eventSource = new EventSource('/api/events');
eventSource.onopen = () => {
console.log('Connection established');
retryCount = 0; // Reset retry count on successful connection
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
function handleError(error) {
console.error('SSE error:', error);
// Close current connection
eventSource.close();
// Try to reconnect with exponential backoff
if (retryCount < maxRetries) {
const timeout = Math.pow(2, retryCount) * 1000;
retryCount++;
console.log(`Reconnecting in ${timeout}ms...`);
setTimeout(() => {
eventSource = new EventSource('/api/events');
// Re-attach event listeners for the new connection
eventSource.onopen = () => {
console.log('Connection established');
retryCount = 0;
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
eventSource.onerror = handleError;
}, timeout);
} else {
console.error('Max retries reached. Giving up.');
// Show an error to the user
}
}
eventSource.onerror = handleError;
Using with Modern Frameworks
React
import { useState, useEffect } from 'react';
function EventComponent() {
const [events, setEvents] = useState([]);
useEffect(() => {
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
const newEvent = JSON.parse(event.data);
setEvents(prevEvents => [newEvent, ...prevEvents].slice(0, 10));
};
// Cleanup function
return () => {
eventSource.close();
};
}, []); // Empty dependency array means this runs once on mount
return (
<div>
<h2>Latest Events</h2>
<ul>
{events.map((event, index) => (
<li key={index}>{event.message}</li>
))}
</ul>
</div>
);
}
Vue 3
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const events = ref([]);
let eventSource = null;
onMounted(() => {
eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
const newEvent = JSON.parse(event.data);
events.value = [newEvent, ...events.value].slice(0, 10);
};
});
onUnmounted(() => {
if (eventSource) {
eventSource.close();
}
});
</script>
<template>
<div>
<h2>Latest Events</h2>
<ul>
<li v-for="(event, index) in events" :key="index">
{{ event.message }}
</li>
</ul>
</div>
</template>
Common Pitfalls and Solutions
1. Maximum Connections Limit
Browsers limit the number of concurrent SSE connections to the same domain (typically 6). Solutions:
- Use a single SSE connection and multiplex different events over it
- Use domain sharding (different subdomains) for multiple connections
- Consider WebSockets if you need many concurrent connections
2. Memory Leaks
SSE connections can cause memory leaks if not properly closed:
// Always close the connection when the component is unmounted
function cleanupSSE() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
}
3. Long Idle Connections
Some proxies or load balancers might close idle connections. Server should send keepalive messages:
// On the client, prepare to handle keep-alive events
eventSource.addEventListener('keepalive', (event) => {
// Just ignore these events, they're just to keep the connection open
console.log('Received keepalive');
});
Performance Considerations
To optimize SSE performance:
- Keep messages small - Send only what's needed
- Batch updates - For high-frequency updates, batch them on the server
- Use compression - Enable gzip for SSE endpoints
- Implement backoff for reconnections - As shown in the reconnection example
- Set appropriate cache headers - To prevent caching of the event stream:
Cache-Control: no-cache
Connection: keep-alive
Conclusion
Server-Sent Events provide a straightforward way to implement real-time updates in web applications. They're perfect for many use cases where you need server-to-client communication without the complexity of WebSockets.
By understanding the patterns and best practices shown in this tutorial, you can create robust applications that respond instantly to backend changes, providing a more dynamic and engaging user experience.
If you have any questions or run into issues implementing SSE in your frontend application, leave a comment below and I'll do my best to help!