From 2f9fadd5f8dc7fbc840e2e56f98b3340c721b808 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Tue, 14 May 2024 06:10:26 +0100 Subject: [PATCH] Fix `Connection closed` error when using `usePresence` hook Resolves #1753 --- .../react-hooks/src/hooks/usePresence.ts | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/platform/react-hooks/src/hooks/usePresence.ts b/src/platform/react-hooks/src/hooks/usePresence.ts index a22d808ba..4e6a44e2c 100644 --- a/src/platform/react-hooks/src/hooks/usePresence.ts +++ b/src/platform/react-hooks/src/hooks/usePresence.ts @@ -1,9 +1,10 @@ import type * as Ably from 'ably'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { ChannelParameters } from '../AblyReactHooks.js'; import { useAbly } from './useAbly.js'; import { useChannelInstance } from './useChannelInstance.js'; import { useStateErrors } from './useStateErrors.js'; +import { useConnectionStateListener } from './useConnectionStateListener.js'; export interface PresenceResult { updateStatus: (messageOrPresenceObject: T) => void; @@ -26,6 +27,7 @@ export function usePresence( const ably = useAbly(params.ablyId); const { channel } = useChannelInstance(params.ablyId, params.channelName); const { connectionError, channelError } = useStateErrors(params); + // we can't simply add messageOrPresenceObject to dependency list in our useCallback/useEffect hooks, // since it will most likely cause an infinite loop of updates in cases when user calls this hook // with an object literal instead of a state or memoized object. @@ -33,30 +35,40 @@ export function usePresence( // note that it still prevents us from automatically re-entering presence with new messageOrPresenceObject if it changes. // one of the options to fix this, is to use deep equals to check if the object has actually changed. see https://github.com/ably/ably-js/issues/1688. const messageOrPresenceObjectRef = useRef(messageOrPresenceObject); - useEffect(() => { messageOrPresenceObjectRef.current = messageOrPresenceObject; }, [messageOrPresenceObject]); - const onMount = useCallback(async () => { - await channel.presence.enter(messageOrPresenceObjectRef.current); - }, [channel.presence]); - - const onUnmount = useCallback(() => { - // if connection is in one of inactive states, leave call will produce exception - if (channel.state === 'attached' && !INACTIVE_CONNECTION_STATES.includes(ably.connection.state)) { - channel.presence.leave(); - } - }, [channel, ably.connection.state]); + // we need to listen for the current connection state in order to react to it. + // for example, we should enter presence when first connected, re-enter when reconnected, + // and be able to prevent entering presence when the connection is in an inactive state. + // all of that can be achieved by using the useConnectionStateListener hook. + const [connectionState, setConnectionState] = useState(ably.connection.state); + useConnectionStateListener((stateChange) => { + setConnectionState(stateChange.current); + }); + const shouldNotEnterPresence = INACTIVE_CONNECTION_STATES.includes(connectionState) || skip; useEffect(() => { - if (skip) return; + if (shouldNotEnterPresence) { + return; + } + const onMount = async () => { + await channel.presence.enter(messageOrPresenceObjectRef.current); + }; onMount(); + return () => { - onUnmount(); + // here we use the ably.connection.state property, which upon this cleanup function call + // will have the current connection state for that connection, thanks to us accessing the Ably instance here by reference. + // if the connection is in one of the inactive states or the channel is not attached, a presence.leave call will produce an exception. + // so we only leave presence in other cases. + if (channel.state === 'attached' && !INACTIVE_CONNECTION_STATES.includes(ably.connection.state)) { + channel.presence.leave(); + } }; - }, [skip, onMount, onUnmount]); + }, [shouldNotEnterPresence, channel, ably.connection.state]); const updateStatus = useCallback( (messageOrPresenceObject: T) => {