-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Can't close fetchEventSource
with AbortController
in React
#84
Comments
You should use |
const useStreamTradingNotifications = () => {
const abortControllerRef = useRef();
useEffect(() => {
console.log("useEffect called");
const stream = async () => {
abortControllerRef.current = new AbortController();
await fetchEventSource("http://127.0.0.1:8000/v1/notifications", {
signal: abortControllerRef.current.signal,
onmessage(msg) {
if (abortControllerRef.current.signal.aborted) {
console.log("still getting messages from aborted signal");
}
console.log(`event: ${msg.event}`);
if (msg.event == "update") {
console.log(JSON.parse(msg.data));
}
},
});
};
stream();
return () => {
console.log("abort called");
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
}; |
|
@daimalou does this actually work for you? This doesn't work for me and I follow the same pattern. |
@bflemi3 This is not my code. just illustrate, when the react component is unmounted, the SSE connection will close. const abortControllerRef = useRef();
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
const startSSE = async () => {
abortControllerRef.current = new AbortController();
await fetchEventSource(url, {
method: 'GET',
signal: abortControllerRef.current.signal,
// ...
}
} |
I had to do something a little different. If my import { useAuth0 } from '@auth0/auth0-react'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { useCallback, useEffect, useRef, useState } from 'react'
import useReffed from './useReffed'
export enum ReadyState {
UNINSTANTIATED = -1,
CONNECTING = 0,
OPEN = 1,
CLOSED = 2,
}
export interface UseStreamOptions<T = unknown> {
onClose?: VoidFunction
onError?: (event: Event) => void
onMessage: (data: T) => void
onOpen?: (event: Response) => void
}
class AbortedError extends Error {}
export default function useStream<T = unknown>(
url: string,
{ onClose, onError, onMessage, onOpen }: UseStreamOptions<T>
) {
const abortControllerRef = useRef<AbortController>(undefined)
const { getAccessTokenSilently } = useAuth0()
// Event Listener Refs
const onCloseRef = useReffed(onClose)
const onErrorRef = useReffed(onError)
const onMessageRef = useReffed(onMessage)
const onOpenRef = useReffed(onOpen)
// State
const [error, setError] = useState(false)
const [readyState, setReadyState] = useState<ReadyState>(ReadyState.UNINSTANTIATED)
useEffect(() => {
if (!url) {
return
}
const abortController = (abortControllerRef.current = new AbortController())
;(async () => {
try {
setReadyState(ReadyState.CONNECTING)
const token = await getAccessTokenSilently()
await fetchEventSource(url, {
headers: { Authorization: `Bearer ${token}` },
signal: abortController.signal,
onclose: () => {
setReadyState(ReadyState.CLOSED)
const onClose = onCloseRef.current
onClose && onClose()
},
onerror: (e) => {
setError(true)
setReadyState(ReadyState.CLOSED)
const onError = onErrorRef.current
onError && onError(e)
// If an error occurs we want to throw to abort the connection
throw e
},
onmessage: (event) => {
onMessageRef.current(data)
},
onopen: async (response) => {
// If the hook was unmounted or disconnected called before this handler, then
// throw the AbortedError to disconnect.
if (abortController.signal.aborted) {
throw new AbortedError()
}
setReadyState(ReadyState.OPEN)
setError(false)
const onOpen = onOpenRef.current
onOpen && onOpen(response)
},
})
} catch (error) {
if (error instanceof AbortedError) {
setReadyState(ReadyState.CLOSED)
} else {
setError(true)
}
}
})()
return () => {
abortController.abort()
}
}, [url])
const disconnect = useCallback(() => {
abortControllerRef.current.abort()
setReadyState(ReadyState.CLOSED)
}, [])
return { error, readyState, disconnect }
} useReffed.ts import { useRef } from 'react'
export default function useReffed<T>(obj: T) {
const ref = useRef(obj)
ref.current = obj
return ref
} |
Hello. I'm aware this topic has been raised many times here already, and I have read all of those issues with no success. Indeed, in most cases those issues have comments from people saying the proposed solution didn't work for them either. I am running my app in
React.StrictMode
, which causes each component to mount/unmount/remount to help identify unexpected side-effects and so on. In my case this is causing twofetchEventSource
requests to be fired, which is expected. What's unexpected, is that using anAbortController
does not close the first connection on unmount. This is my hook:When I run my app, I see the following logs:
As you can see:
useEffect
is called twice, as the component is mounted/unmounted/remounted [expected]abortController.abort
is called when the first component unmounts [expected]still getting messages from aborted signal
).Furthermore, in my backend logs, when I close a browser window with an active connection I see a log like:
But I never see this message as the result of
AbortController.abort
being called. I have tried assigning a new instance ofAbortController
after abort (as suggested here), which had no affect. I have tried doing a similar thing withsetState
(as mentioned here) which made the problem much worse (kept creating connections until the browser crashed). I tried to useuseRef
to manage the controller (suggested here, which also doesn't work.The fact that I can see
abort
being called, but never see ahttp.disconnect
on the backend, convinces me this must be a bug. But if anybody has a suspicion that I've screwed something up I'd really appreciate their suggestion.Many thanks
The text was updated successfully, but these errors were encountered: