Skip to content

Commit ed0983c

Browse files
fix(react-bindings): getSnapshot caching (#2008)
Fixes #2006. Cold-derived `Observable` instances (`remoteParticipants$` and similar) get read twice in strict mode, and as such, the derivative runs multiple times, creating multiple shallow-equal arrays. Although this isn't exactly an issue, as we had this behavior before #1953, the emitter warning could be confusing. Docs: GetStream/docs-content#778 --------- Co-authored-by: Anton Arnautov <[email protected]>
1 parent a3bc5b8 commit ed0983c

File tree

2 files changed

+33
-2
lines changed

2 files changed

+33
-2
lines changed

packages/client/src/store/CallState.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,29 @@ type OrphanedTrack = {
7474
track: MediaStream;
7575
};
7676

77+
/**
78+
* Creates a stable participant filter function, ready to be used in combination
79+
* with the `useSyncExternalStore` hook.
80+
*
81+
* @param predicate the predicate to use.
82+
*/
83+
const createStableParticipantsFilter = (
84+
predicate: (p: StreamVideoParticipant) => boolean,
85+
) => {
86+
const empty: StreamVideoParticipant[] = [];
87+
return (participants: StreamVideoParticipant[]) => {
88+
// no need to filter if there are no participants
89+
if (!participants.length) return participants;
90+
91+
// return a stable empty array if there are no remote participants
92+
// instead of creating an empty one
93+
const filteredParticipants = participants.filter(predicate);
94+
if (!filteredParticipants.length) return empty;
95+
96+
return filteredParticipants;
97+
};
98+
};
99+
77100
/**
78101
* Holds the state of the current call.
79102
* @react You don't have to use this class directly, as we are exposing the state through Hooks.
@@ -353,12 +376,12 @@ export class CallState {
353376
);
354377

355378
this.remoteParticipants$ = this.participants$.pipe(
356-
map((participants) => participants.filter((p) => !p.isLocalParticipant)),
379+
map(createStableParticipantsFilter((p) => !p.isLocalParticipant)),
357380
shareReplay({ bufferSize: 1, refCount: true }),
358381
);
359382

360383
this.pinnedParticipants$ = this.participants$.pipe(
361-
map((participants) => participants.filter((p) => !!p.pin)),
384+
map(createStableParticipantsFilter((p) => !!p.pin)),
362385
shareReplay({ bufferSize: 1, refCount: true }),
363386
);
364387

packages/react-bindings/src/hooks/callStateHooks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ export const useRemoteParticipants = () => {
293293
return useObservableValue(remoteParticipants$);
294294
};
295295

296+
/**
297+
* A hook which provides a list of participants that are currently pinned.
298+
*/
299+
export const usePinnedParticipants = () => {
300+
const { pinnedParticipants$ } = useCallState();
301+
return useObservableValue(pinnedParticipants$);
302+
};
303+
296304
/**
297305
* Returns the approximate participant count of the active call.
298306
* This includes the anonymous users as well, and it is computed on the server.

0 commit comments

Comments
 (0)