Real-time updates are easy to demonstrate. Making them work reliably in a real application is a different story.
You need to manage the connection lifecycle, handle authentication, decide what happens after a network interruption, and keep your UI in sync with server state.
Server-Sent Events (SSE) are a good fit when the server needs to push updates to the client over HTTP. But how should you integrate them into a React application that already uses TanStack Query?
1. Start with the right responsibilities
I like to separate real-time data into three concerns:
- Transport: Establish the stream, receive events, and handle connection lifecycle.
- Server state: Store and update the data the UI consumes.
- Recovery: Decide whether to replay missed events or fetch the latest state again.
TanStack Query already handles much of the server-state lifecycle. The SSE connection should complement it, not become a competing cache.
2. Why not just use EventSource?
The native browser API is a reasonable starting point:
const source = new EventSource('/api/orders/stream');
source.onmessage = (event) => {
console.log(JSON.parse(event.data));
};
source.close();
However, native EventSource doesn't let you attach arbitrary Authorization headers. That becomes a limitation when your API relies on bearer-token authentication.
A fetch-based SSE client can provide more control over request configuration and authentication.
3. Using tanstack-fetch for SSE
The framework-agnostic entry point is tanstack-fetch/sse.
import { createFetch } from 'tanstack-fetch/sse';
type OrderEvent = {
id: string;
status: string;
};
const api = createFetch({
baseUrl: 'https://api.example.com',
plugins: ['sse-resume'],
getToken: () => localStorage.getItem('access_token'),
});
const stream = api.sse<OrderEvent>('/orders/stream', {
onMessage: (data) => {
console.log('Order update:', data);
},
});
// When the stream is no longer needed:
stream.close();
This example uses the documented client setup and streaming API. The actual event schema and server behavior depend on your backend.
The key benefit of this approach is that the stream can use a fetch-based transport rather than being limited to the native EventSource interface.
4. React integration with useSse
For React applications, the library also provides a framework-specific hook.
import { createFetch } from 'tanstack-fetch/sse';
import { useSse } from 'tanstack-fetch/react';
const api = createFetch({
baseUrl: import.meta.env.VITE_API_URL,
plugins: ['sse-resume'],
getToken: () => localStorage.getItem('access_token'),
});
type OrderEvent = {
id: string;
status: string;
};
function OrdersLive() {
const { data, status, error, close } =
useSse<OrderEvent>('/orders/stream', {
client: api,
});
if (status === 'error') {
return <p>{error?.message}</p>;
}
return (
<section>
<p>Connection: {status}</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
<button type="button" onClick={close}>
Disconnect
</button>
</section>
);
}
The hook exposes the latest data, connection status, error information, and a way to close the stream.
For Next.js App Router, use the hook inside a Client Component. Don't open browser-dependent streams inside server-side loaders or SSR code.
5. Where does TanStack Query fit?
A stream and a query solve different problems.
A query is useful for retrieving the initial state and recovering the latest server snapshot. SSE is useful for receiving changes as they happen.
For example, you could fetch an order using useQuery, then update the cache when a complete order snapshot arrives:
queryClient.setQueryData(
['order', orderId],
updatedOrder,
);
If an event only signals that the order changed, invalidating the query may be safer:
queryClient.invalidateQueries({
queryKey: ['order', orderId],
});
The right choice depends on your event contract. A partial event should not blindly replace a complete cached object.
6. Reconnection versus resume
These terms are often used interchangeably, but they describe different things.
Reconnection means opening the connection again after it fails.
Resume means continuing from a known position in the event stream so the client can recover events it missed.
A reconnecting client can still lose updates if the server doesn't support replay. Reliable resume generally requires event identifiers, server-side history, and a way to request events after the last processed position.
The sse-resume plugin is available in tanstack-fetch. Before relying on it in production, verify its exact behavior against your library version and the replay protocol implemented by your server.
7. When should you use SSE?
SSE is worth considering for:
- Notification feeds
- Order status changes
- Live dashboards
- Job progress indicators
- Server-to-client event streams
If you need bidirectional communication, WebSockets may be a better fit. If updates are infrequent, ordinary queries or polling may be simpler.
Final thought
Real-time functionality isn't just about opening a stream. It's about deciding how transport, server state, authentication, and recovery work together.
I'm interested in how other React developers approach this:
Do you update the TanStack Query cache directly from SSE events, invalidate queries, or keep a separate event-driven state layer?
Docs and examples: https://mohamadgarmabi.github.io/tanstack-fetch/guide/sse
Disclosure: I'm the author of tanstack-fetch, an independent package and not an official TanStack project.

Top comments (0)