Supporting a stream-based flow (EventSource, SSE) #418
Replies: 6 comments 18 replies
-
Let me start with this... if you can write a custom hook that uses the |
Beta Was this translation helpful? Give feedback.
-
Moving this to discussions as this is a feature request. |
Beta Was this translation helpful? Give feedback.
-
So I tried this that works OK, but I feel there is a better way: The API would be something like: const { status, data } = useEventSourceQuery(myQueryKey, myEventSourceURL, myEventName); as for the implementation I thought of something like: import * as React from 'react';
import { useInfiniteQuery} from 'react-query';
interface CustomQueryParams {
eventData: string[];
}
type Status = "success" | "loading" | "error";
export const useEventSourceQuery = (key: string, url: string, eventName: string) => {
const eventSource = React.useRef<EventSource>(new EventSource(url));
const queryFn = React.useCallback((_: any, params: CustomQueryParams) => {
if (!params) { return Promise.resolve([])}
return Promise.resolve(params.eventData);
}, [])
const { data, fetchMore } = useInfiniteQuery<string[], string, CustomQueryParams>(key, queryFn as any, { getFetchMore: () => ({ eventData: []})})
const customStatus = React.useRef<Status>('success');
React.useEffect(() => {
const evtSource = eventSource.current;
const onEvent = function (ev: MessageEvent | Event) {
if (!e.data) {
return;
}
// Let's assume here we receive multiple data, ie. e.data is an array.
fetchMore({ eventData: e.data });
}
const onError = () => { customStatus.current = 'error' };
evtSource.addEventListener(eventName, onEvent);
evtSource.addEventListener('error', onError);
return () => {
evtSource.removeEventListener(eventName,onEvent);
evtSource.removeEventListener('error', onError);
}
}, [url, eventName, fetchMore])
return {status: customStatus.current, data };
} Some critical thoughts regarding this: I'm still unsure if this is the way to go. Does the URL and event name should live in this hook, like is it something this hook should handle OR should we pass some kind of async generator function so that the hook does not even know about EventSource - ie. it could work for anything async other than simple promise. WDYT ? |
Beta Was this translation helpful? Give feedback.
-
Soo based on your latest suggestion, I made two rough things: One using async generator to pull data: export const useAyncGeneratorQuery = (key: string, asyncGeneratorFn: () => Generator<string[], string[], unknown>) => {
const queryFn = React.useCallback((_: any, params: CustomQueryParams) => {
if (!params) { return Promise.resolve([])}
return Promise.resolve(params.eventData);
}, [])
const { data, fetchMore } = useInfiniteQuery<string[], string, CustomQueryParams>(key, queryFn as any, { getFetchMore: () => ({ eventData: []})})
const customStatus = React.useRef<Status>('success');
React.useEffect(() => {
(async function doReceive() {
try {
for await (let data of asyncGeneratorFn()) {
fetchMore({ eventData: data });
}
} catch (e) {
customStatus.current = 'error';
}
})();
}, [asyncGeneratorFn, fetchMore])
return {status: customStatus.current, data };
} and one using simple callback paradigm: type CallbackType = (data: string[]) => void;
type InitCallbackType = (cb: CallbackType) => void;
export const useCallbackQuery = (key: string, initCallbackQuery: InitCallbackType) => {
const queryFn = React.useCallback((_: any, params: CustomQueryParams) => {
if (!params) { return Promise.resolve([])}
return Promise.resolve(params.eventData);
}, [])
const { data, fetchMore } = useInfiniteQuery<string[], string, CustomQueryParams>(key, queryFn as any, { getFetchMore: () => ({ eventData: []})})
const callback = React.useCallback((data) => { fetchMore({ eventData: data })}, [fetchMore]);
React.useEffect(() => { initCallbackQuery(callback); }, [callback, initCallbackQuery] )
const customStatus = React.useRef<Status>('success');
return {status: customStatus.current, data };
} I said rough because both are not handling errors and/or maybe edge-cases. They also have an initial empty array. You can check the live code there https://codesandbox.io/s/interesting-bhaskara-zw2dd?file=/src/App.tsx . Can you think of anything that would break things / edge cases I should handle ? Also, when clicking on the sandbox browser pane, everything seems to be cleared somehow - not sure if related to |
Beta Was this translation helpful? Give feedback.
-
I have found a simpler solution using the queryClient API and updating the data from the event source events. Maybe this is an anti pattern for react-query but it works nicely for me. Something along the lines of: import { useQuery, useQueryClient } from 'react-query'
export const useEventSourceQuery = (queryKey, url, eventName) => {
const queryClient = useQueryClient()
const fetchData = () => {
const evtSource = new EventSource(url)
evtSource.addEventListener(eventName, (event) => {
const eventData = event.data && JSON.parse(event.data)
if (eventData) {
queryClient.setQueryData(queryKey, eventData)
}
})
}
return useQuery(queryKey, fetchData)
} |
Beta Was this translation helpful? Give feedback.
-
Just for anyone landing here today, most browsers now support fetch streams So there is no need to use Here's a package from Microsoft that does exactly this https://github.com/Azure/fetch-event-source So you could just swap out your fetch function for this one, and you get SSE support inside react query |
Beta Was this translation helpful? Give feedback.
-
Hi!
Just curious if you would be interested in supporting a new use case for streamed data.
For instance, it could support EventSource.
From what I can foresee, this would "look-like" the infinite query flow - with the difference being that it's not the front-end pulling new data but the server sending it intermittently.
Also, just wanted to say I find the doc really nice 👍 . Great job!
Beta Was this translation helpful? Give feedback.
All reactions