Skip to content

Commit

Permalink
Fix Connection closed error when using usePresence hook
Browse files Browse the repository at this point in the history
Resolves #1753
  • Loading branch information
VeskeR committed May 24, 2024
1 parent cee64bd commit 2f9fadd
Showing 1 changed file with 27 additions and 15 deletions.
42 changes: 27 additions & 15 deletions src/platform/react-hooks/src/hooks/usePresence.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
updateStatus: (messageOrPresenceObject: T) => void;
Expand All @@ -26,37 +27,48 @@ export function usePresence<T = any>(
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.
// to prevent this from happening we store messageOrPresenceObject in a ref, and use that instead.
// 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) => {
Expand Down

0 comments on commit 2f9fadd

Please sign in to comment.