Skip to content

Commit

Permalink
fix(clerk-react): Handle multiple addListener calls (#4010)
Browse files Browse the repository at this point in the history
  • Loading branch information
wobsoriano authored Aug 23, 2024
1 parent edbb1d4 commit 96234ce
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/itchy-tigers-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/clerk-react": patch
---

Fix multiple `addListener` method calls
51 changes: 51 additions & 0 deletions packages/react/src/__tests__/isomorphicClerk.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Resources, UnsubscribeCallback } from '@clerk/types';

import { IsomorphicClerk } from '../isomorphicClerk';

describe('isomorphicClerk', () => {
Expand Down Expand Up @@ -49,4 +51,53 @@ describe('isomorphicClerk', () => {
{ appearance: { baseTheme: 'white' } },
]);
});

it('handles multiple resource listeners', async () => {
const listenerCallHistory: Array<Resources> = [];
const addedListeners: Map<(payload: Resources) => void, { unsubscribe: UnsubscribeCallback }> = new Map();

const dummyClerkJS = {
addListener: (listener: (payload: Resources) => void) => {
const unsubscribe = () => {
addedListeners.delete(listener);
};
addedListeners.set(listener, { unsubscribe });
return unsubscribe;
},
};

const isomorphicClerk = new IsomorphicClerk({ publishableKey: 'pk_test_xxx' });
(isomorphicClerk as any).clerkjs = dummyClerkJS as any;

const unsubscribe1 = isomorphicClerk.addListener(payload => listenerCallHistory.push(payload));
const unsubscribe2 = isomorphicClerk.addListener(payload => listenerCallHistory.push(payload));

// Unsubscribe one listener before ClerkJS is loaded
unsubscribe1();

jest.spyOn(isomorphicClerk, 'loaded', 'get').mockReturnValue(true);
isomorphicClerk.emitLoaded();
const unsubscribe3 = isomorphicClerk.addListener(payload => listenerCallHistory.push(payload));

// Simulate ClerkJS triggering the listeners
const mockPayload = {
user: { id: 'user_xxx' },
session: { id: 'sess_xxx' },
client: { id: 'client_xxx' },
organization: undefined,
} as Resources;
addedListeners.forEach((_, listener) => listener(mockPayload));

expect(listenerCallHistory).toEqual([mockPayload, mockPayload]);
expect(listenerCallHistory.length).toBe(2);

// Unsubscribe all remaining listeners
unsubscribe2();
unsubscribe3();
listenerCallHistory.length = 0;
addedListeners.forEach((_, listener) => listener(mockPayload));

expect(listenerCallHistory).toEqual([]);
expect(listenerCallHistory.length).toBe(0);
});
});
26 changes: 21 additions & 5 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
private premountOrganizationSwitcherNodes = new Map<HTMLDivElement, OrganizationSwitcherProps>();
private premountOrganizationListNodes = new Map<HTMLDivElement, OrganizationListProps>();
private premountMethodCalls = new Map<MethodName<BrowserClerk>, MethodCallback>();
// A separate Map of `addListener` method calls to handle multiple listeners.
private premountAddListenerCalls = new Map<
ListenerCallback,
{
unsubscribe: UnsubscribeCallback;
nativeUnsubscribe?: UnsubscribeCallback;
}
>();
private loadedListeners: Array<() => void> = [];

#loaded = false;
Expand Down Expand Up @@ -477,6 +485,9 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
this.clerkjs = clerkjs;

this.premountMethodCalls.forEach(cb => cb());
this.premountAddListenerCalls.forEach((listenerHandlers, listener) => {
listenerHandlers.nativeUnsubscribe = clerkjs.addListener(listener);
});

if (this.preopenSignIn !== null) {
clerkjs.openSignIn(this.preopenSignIn);
Expand Down Expand Up @@ -834,13 +845,18 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
};

addListener = (listener: ListenerCallback): UnsubscribeCallback => {
const callback = () => this.clerkjs?.addListener(listener);

if (this.clerkjs) {
return callback() as UnsubscribeCallback;
return this.clerkjs.addListener(listener);
} else {
this.premountMethodCalls.set('addListener', callback);
return () => this.premountMethodCalls.delete('addListener');
const unsubscribe = () => {
const listenerHandlers = this.premountAddListenerCalls.get(listener);
if (listenerHandlers) {
listenerHandlers.nativeUnsubscribe?.();
this.premountAddListenerCalls.delete(listener);
}
};
this.premountAddListenerCalls.set(listener, { unsubscribe, nativeUnsubscribe: undefined });
return unsubscribe;
}
};

Expand Down

0 comments on commit 96234ce

Please sign in to comment.