React SSE with TypeScript: Authentication, Connection Status, and TanStack Query

Server-Sent Events are useful when a server needs to push updates to a browser without requiring the client to poll continuously.
Common examples include live notifications, order updates, monitoring dashboards, and background job progress.
In this tutorial, we'll look at a fetch-based SSE client, React integration, and strategies for keeping server state consistent with TanStack Query.
1. Why fetch-based SSE?
The browser's native EventSource API is easy to use:
const source = new EventSource('/api/events');
However, it doesn't allow arbitrary request headers, which can be a limitation when an API requires bearer-token authentication.
A fetch-based SSE implementation gives the client more control over request configuration.
2. Install and configure the client
Install the package using your preferred package manager:
npm install tanstack-fetch
Configure the client using the documented SSE entry point:
import { createFetch } from 'tanstack-fetch/sse';
const api = createFetch({
baseUrl: 'https://api.example.com',
plugins: ['sse-resume'],
getToken: () => localStorage.getItem('access_token'),
});
Use a token strategy appropriate for your application. Browser storage, token rotation, and authentication failure handling require security considerations beyond the stream itself.
3. Create a typed stream
Define the shape of the events your backend sends:
type OrderEvent = {
id: string;
status: 'pending' | 'paid' | 'shipped';
};
Open the stream:
const stream = api.sse<OrderEvent>('/orders/stream', {
onMessage: (data) => {
console.log(data.id, data.status);
},
});
Close it when you no longer need it:
stream.close();
The generic type helps TypeScript describe the expected event data. It does not validate untrusted JSON at runtime, so validate payloads when necessary.
4. Use the React hook
The React adapter exposes a useSse hook:
import { createFetch } from 'tanstack-fetch/sse';
import { useSse } from 'tanstack-fetch/react';
const api = createFetch({
plugins: ['sse-resume'],
});
function LiveOrders() {
const { data, status, error, close } =
useSse<OrderEvent>('/orders/stream', {
client: api,
});
if (status === 'error') {
return <p>{error?.message}</p>;
}
return (
<main>
<p>Connection status: {status}</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
<button type="button" onClick={close}>
Close connection
</button>
</main>
);
}
The documented status values include connecting, connected, disconnected, and error.
For Next.js App Router, use this hook in a Client Component. Browser-dependent streams should not be opened from server-side loaders.
5. Keep TanStack Query responsible for cached state
Suppose your application already loads an order with useQuery.
When the stream delivers a complete updated order, you can update the relevant query cache:
queryClient.setQueryData(
['order', orderId],
updatedOrder,
);
When the event only signals that the order changed, invalidate the query instead:
queryClient.invalidateQueries({
queryKey: ['order', orderId],
});
Choose based on the event contract. Don't replace a complete cached object with a partial payload.
6. Handle disconnections correctly
A connection can be re-established without recovering every event that was missed while it was offline.
Reliable resume generally needs:
Stable event IDs.
Server-side event retention.
A replay mechanism.
Duplicate-event handling.
A fallback for expired event history.
tanstack-fetch provides an sse-resume plugin. Verify the plugin's documented behavior and the server's replay support before depending on exactly-once processing or uninterrupted event history.
For applications that only care about the latest state, refetching after a disconnection can be simpler. For audit logs and workflows where every event matters, replay is more important.
7. When should you use SSE?
Choose SSE when updates flow primarily from the server to the client.
Use polling when updates are infrequent and simplicity matters. Consider WebSockets when the application needs continuous two-way communication.
The best architecture is the one that matches your data consistency requirements without adding unnecessary state-management layers.
Resources
tanstack-fetch is an independent project by Mohammad Garmabi and is not an official TanStack package.
Question: Do you prefer direct query-cache updates from SSE or invalidation followed by a refetch?
