Using Server-Side Events with Spring Boot and React for Immediate Notifications
- Mark Kendall
- Feb 11
- 5 min read
Using Server-Sent Events with Spring Boot and React for Immediate Notifications
This article demonstrates how to build a real-time notification system using Server-Sent Events (SSE) with a Spring Boot backend and a React frontend, enabling immediate notifications. SSE is a lightweight, unidirectional communication protocol that allows the server to push updates to the client without explicit client requests, perfect for real-time notifications.
1. Spring Boot Backend:
We'll create a Spring Boot REST controller to handle SSE connections and send events to connected clients.
```java
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class NotificationController {
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
private final ExecutorService executor = Executors.newFixedThreadPool(10); // Adjust pool size as needed
@GetMapping(value = "/notifications", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamNotifications(@RequestParam String userId) {
SseEmitter emitter = new SseEmitter();
emitters.put(userId, emitter);
emitter.onCompletion(() -> emitters.remove(userId));
emitter.onError(e -> {
emitters.remove(userId);
e.printStackTrace(); // Use a proper logger in production
});
emitter.onTimeout(() -> {
emitters.remove(userId);
emitter.complete();
});
try {
emitter.send(SseEmitter.event().name("connected").data("Connection established!"));
} catch (IOException e) {
e.printStackTrace(); // Log the error
}
return emitter;
}
@PostMapping("/sendEvent") // Use POST for sending events
public String sendEvent(@RequestParam String userId, @RequestBody String message) { // Use @RequestBody
SseEmitter emitter = emitters.get(userId);
if (emitter != null) {
executor.execute(() -> {
try {
emitter.send(SseEmitter.event().name("notification").data(message)); // Named event
} catch (IOException e) {
emitters.remove(userId);
e.printStackTrace(); // Log the error
}
});
return "Event sent!";
}
return "No emitter found for this user.";
}
// Example of triggering an event from a service (or any other part of your application)
// @Service
// public class MyService {
// @Autowired
// private NotificationController notificationController;
// public void doSomething() {
// String userId = "123"; // Get the user ID
// String message = "Something happened!";
// notificationController.sendEvent(userId, message); // Call the controller
// }
//}
}
```
Key Changes and Explanations (Backend):
*`@PostMapping` for `/sendEvent`:** Using `POST` is generally better for sending data to the server, as it's more appropriate for actions that modify server state.
*`@RequestBody`:** This annotation allows you to send the message in the request body (e.g., as JSON), which is cleaner and more flexible than using query parameters for the message content.
*Named Event:** The event is now sent with a name ("notification"). This is highly recommended as it allows the client to distinguish between different types of events.
*Example Service Call:** The commented-out `MyService` example shows how you would call `sendEvent` from another part of your application (e.g., a service, scheduled task, or after some action occurs).
2. React Frontend:
```javascript
import React, { useState, useEffect } from 'react';
function Notifications() {
const [messages, setMessages] = useState([]);
const userId = "123"; // Replace with actual user ID
useEffect(() => {
if (!userId) return;
const eventSource = new EventSource(`/notifications?userId=${userId}`);
eventSource.onmessage = (event) => {
console.log("Received event:", event);
if (event.data === "Connection established!") {
console.log("SSE connection established");
return;
}
if (event.type === "notification") { // Check event type
try {
const message = JSON.parse(event.data); // Parse if JSON
setMessages((prevMessages) => [...prevMessages, message]);
} catch (error) {
setMessages((prevMessages) => [...prevMessages, event.data]); // Fallback to plain text
}
}
};
eventSource.onerror = (error) => {
console.error("SSE error:", error);
eventSource.close();
};
return () => {
eventSource.close();
};
}, [userId]);
const sendTestEvent = async () => {
const message = { text: "Hello from React!", value: 123 }; // Example JSON message
try {
const response = await fetch(`/sendEvent?userId=${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Important: Set Content-Type
},
body: JSON.stringify(message), // Send message in the body
});
if (response.ok) {
console.log("Test event sent successfully");
} else {
console.error("Failed to send test event");
}
} catch (error) {
console.error("Error sending test event:", error);
}
};
return (
<div>
<h2>Notifications</h2>
<button onClick={sendTestEvent}>Send Test Event</button>
<ul>
{messages.map((message, index) => (
<li key={index}>{message.text || message}</li> // Display message
))}
</ul>
</div>
);
}
export default Notifications;
```
Key Changes and Explanations (Frontend):
*`POST` Request:** The `sendTestEvent` function now uses a `POST` request to `/sendEvent`.
*`Content-Type` Header:** The `Content-Type` header is set to `application/json` to indicate that you are sending JSON data in the request body. This is crucial.
*`JSON.stringify()`:** The message is converted to a JSON string using `JSON.stringify()` before being sent in the request body.
*Handling Named Events:** The `onmessage` handler now checks `event.type === "notification"` to ensure it's handling the correct type of event. This is essential when you have multiple event types.
*Display Logic:** The display logic now correctly handles the JSON message format (`message.text`).
This revised version provides a more robust and practical implementation of server-sent events with Spring Boot and React, including best practices for sending events (using `POST` and JSON), handling different event types, and managing the SSE connection effectively. It's now much closer to a production-ready implementation.
Comentários