feat(core): add useSSEStream — consume fetch-based SSE / streaming responses with POST support
Clear and concise description of the problem
Summary
VueUse already ships useEventSource, but it wraps the native EventSource, which is GET-only — no request body, no custom headers (e.g. Authorization). That makes it impossible to consume modern streaming APIs, most notably LLM chat endpoints (OpenAI / DeepSeek-compatible), which require POST + Authorization and respond with text/event-stream.
useFetch does have a stream: true option, but as reported in #5236 it buffers the entire response and never exposes incremental reads of response.body, so it cannot power token-by-token UIs either.
Proposal
Add a new core composable that consumes a fetch-based streaming response via ReadableStream, parses SSE frames, and exposes the stream reactively.
Working name: useSSEStream (alternatives: useEventStream, useFetchStream).
Suggested solution
const { data, // ShallowRef — latest chunk / accumulated payload status, // Ref<'idle' | 'loading' | 'streaming' | 'success' | 'error'> error, // ShallowRef isStreaming, // ComputedRef start, // (payload?: unknown) => void — start / restart the stream abort, // () => void — abort the current stream close, // () => void — abort and reset } = useSSEStream('/api/chat', { method: 'POST', headers: { Authorization: 'Bearer ...' }, body: payload, // MaybeRefOrGetter autoStart: false, onEvent: (event, data) => { /* per-frame callback */ }, })
Alternative
Extending useFetch to expose incremental reads of response.body would fill the same gap (it's the root cause described in #5236), but it's a more invasive change to a widely-used composable and carries higher regression risk. Alternative names: useEventStream or useFetchStream. Alternative placement: @vueuse/integrations instead of @vueuse/core. For the return shape, another option is exposing raw parsed events per-frame rather than an accumulated string, or both behind an option.
Additional context
I hit this gap while building an AI assistant in Vue 3 against a DeepSeek-compatible API: data updates reactively as chunks arrive, isStreaming drives the loading state, and abort() enables "stop generation".
const { data, isStreaming, start, abort } = useSSEStream('/api/chat', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: { messages: [...messages.value] },
autoStart: false,
})
const send = () => start()
Questions for maintainers: is this scope appropriate for @vueuse/core or better in @vueuse/integrations? Should we expose raw parsed events, an accumulated string, or both (configurable)? Naming preference? Happy to implement once the direction is confirmed. Related: #5236.
### Validations
- [x] Follow our [Code of Conduct](https://github.com/vueuse/vueuse/blob/main/CODE_OF_CONDUCT.md)
- [x] Read the [Contributing Guidelines](https://github.com/vueuse/vueuse/blob/main/CONTRIBUTING.md).
- [x] Read the [docs](https://vueuse.org/guide).
- [x] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
Source: vueuse/vueuse