Skip to content

Commit 6d960a8

Browse files
authored
fix(vue): Prevent TDZ error in watch callbacks (#7743)
1 parent 9c56d1a commit 6d960a8

7 files changed

Lines changed: 184 additions & 17 deletions

File tree

.changeset/red-shoes-drum.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/vue": patch
3+
---
4+
5+
Fixed an error occurring in the composables where watchers attempted to call unwatch() within their own initialization.

packages/vue/src/composables/useAuth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ import { useClerkContext } from './useClerkContext';
1313
*/
1414
function clerkLoaded(clerk: ShallowRef<Clerk | null>) {
1515
return new Promise<Clerk>(resolve => {
16-
watch(
16+
let unwatch: (() => void) | undefined;
17+
// eslint-disable-next-line prefer-const
18+
unwatch = watch(
1719
clerk,
1820
value => {
1921
if (value?.loaded) {
2022
resolve(value);
23+
unwatch?.();
2124
}
2225
},
2326
{ immediate: true },

packages/vue/src/composables/useOrganization.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const useOrganization: UseOrganization = () => {
5656
const { clerk, organizationCtx } = useClerkContext('useOrganization');
5757
const { session } = useSession();
5858

59-
const unwatch = watch(
59+
watch(
6060
clerk,
6161
value => {
6262
if (value) {
@@ -65,10 +65,9 @@ export const useOrganization: UseOrganization = () => {
6565
for: 'organizations',
6666
caller: 'useOrganization',
6767
});
68-
unwatch();
6968
}
7069
},
71-
{ immediate: true },
70+
{ once: true },
7271
);
7372

7473
const result = computed<UseOrganizationReturn>(() => {

packages/vue/src/composables/useSignIn.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ type UseSignIn = () => ToComputedRefs<UseSignInReturn>;
3232
export const useSignIn: UseSignIn = () => {
3333
const { clerk, clientCtx } = useClerkContext('useSignIn');
3434

35-
const unwatch = watch(clerk, value => {
36-
if (value) {
37-
value.telemetry?.record(eventMethodCalled('useSignIn'));
38-
unwatch();
39-
}
40-
});
35+
watch(
36+
clerk,
37+
value => {
38+
if (value) {
39+
value.telemetry?.record(eventMethodCalled('useSignIn'));
40+
}
41+
},
42+
{ once: true },
43+
);
4144

4245
const result = computed<UseSignInReturn>(() => {
4346
if (!clerk.value || !clientCtx.value) {

packages/vue/src/composables/useSignUp.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ type UseSignUp = () => ToComputedRefs<UseSignUpReturn>;
3232
export const useSignUp: UseSignUp = () => {
3333
const { clerk, clientCtx } = useClerkContext('useSignUp');
3434

35-
const unwatch = watch(clerk, value => {
36-
if (value) {
37-
value.telemetry?.record(eventMethodCalled('useSignUp'));
38-
unwatch();
39-
}
40-
});
35+
watch(
36+
clerk,
37+
value => {
38+
if (value) {
39+
value.telemetry?.record(eventMethodCalled('useSignUp'));
40+
}
41+
},
42+
{ once: true },
43+
);
4144

4245
const result = computed<UseSignUpReturn>(() => {
4346
if (!clerk.value || !clientCtx.value) {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { Clerk } from '@clerk/shared/types';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { nextTick, ref, type ShallowRef } from 'vue';
4+
5+
import * as composables from '../../composables';
6+
import { useClerkLoaded } from '../useClerkLoaded';
7+
8+
// Mock the useClerk composable
9+
vi.mock('../../composables', () => ({
10+
useClerk: vi.fn(),
11+
}));
12+
13+
describe('useClerkLoaded', () => {
14+
let clerkRef: ShallowRef<Clerk | null>;
15+
let mockClerk: Partial<Clerk>;
16+
17+
beforeEach(() => {
18+
clerkRef = ref(null) as ShallowRef<Clerk | null>;
19+
mockClerk = {
20+
loaded: false,
21+
};
22+
vi.mocked(composables.useClerk).mockReturnValue(clerkRef);
23+
});
24+
25+
it('should not call callback when clerk is null', async () => {
26+
const callback = vi.fn();
27+
28+
// Call useClerkLoaded
29+
useClerkLoaded(callback);
30+
31+
await nextTick();
32+
33+
expect(callback).not.toHaveBeenCalled();
34+
});
35+
36+
it('should not call callback when clerk exists but not loaded', async () => {
37+
const callback = vi.fn();
38+
39+
// Call useClerkLoaded
40+
useClerkLoaded(callback);
41+
42+
// Set clerk instance but not loaded
43+
clerkRef.value = { ...mockClerk, loaded: false } as Clerk;
44+
45+
await nextTick();
46+
47+
expect(callback).not.toHaveBeenCalled();
48+
});
49+
50+
it('should call callback when clerk becomes loaded', async () => {
51+
const callback = vi.fn();
52+
53+
// Call useClerkLoaded
54+
useClerkLoaded(callback);
55+
56+
// Set clerk instance as loaded
57+
const loadedClerk = { ...mockClerk, loaded: true } as Clerk;
58+
clerkRef.value = loadedClerk;
59+
60+
await nextTick();
61+
62+
expect(callback).toHaveBeenCalledOnce();
63+
expect(callback).toHaveBeenCalledWith(loadedClerk);
64+
});
65+
66+
it('should call callback immediately if clerk is already loaded', async () => {
67+
const callback = vi.fn();
68+
69+
// Set clerk instance as loaded before calling useClerkLoaded
70+
const loadedClerk = { ...mockClerk, loaded: true } as Clerk;
71+
clerkRef.value = loadedClerk;
72+
73+
// Call useClerkLoaded
74+
useClerkLoaded(callback);
75+
76+
await nextTick();
77+
78+
expect(callback).toHaveBeenCalledOnce();
79+
expect(callback).toHaveBeenCalledWith(loadedClerk);
80+
});
81+
82+
it('should only call callback once even if clerk updates multiple times', async () => {
83+
const callback = vi.fn();
84+
85+
// Call useClerkLoaded
86+
useClerkLoaded(callback);
87+
88+
// Set clerk instance as loaded
89+
const loadedClerk1 = { ...mockClerk, loaded: true, client: { id: 'client_1' } } as Clerk;
90+
clerkRef.value = loadedClerk1;
91+
92+
await nextTick();
93+
94+
expect(callback).toHaveBeenCalledOnce();
95+
96+
// Update clerk instance again (simulating a resource update)
97+
const loadedClerk2 = { ...mockClerk, loaded: true, client: { id: 'client_2' } } as Clerk;
98+
clerkRef.value = loadedClerk2;
99+
100+
await nextTick();
101+
102+
// Should still only be called once due to unwatch()
103+
expect(callback).toHaveBeenCalledOnce();
104+
expect(callback).toHaveBeenCalledWith(loadedClerk1);
105+
});
106+
107+
it('should handle transition from null -> not loaded -> loaded', async () => {
108+
const callback = vi.fn();
109+
110+
// Call useClerkLoaded
111+
useClerkLoaded(callback);
112+
113+
// Initial state: null
114+
expect(callback).not.toHaveBeenCalled();
115+
116+
// Clerk instance created but not loaded
117+
clerkRef.value = { ...mockClerk, loaded: false } as Clerk;
118+
await nextTick();
119+
expect(callback).not.toHaveBeenCalled();
120+
121+
// Clerk becomes loaded
122+
clerkRef.value = { ...mockClerk, loaded: true } as Clerk;
123+
await nextTick();
124+
125+
expect(callback).toHaveBeenCalledOnce();
126+
});
127+
128+
it('should properly clean up watcher after callback is called', async () => {
129+
const callback = vi.fn();
130+
131+
// Call useClerkLoaded
132+
useClerkLoaded(callback);
133+
134+
// Set clerk as loaded
135+
const loadedClerk = { ...mockClerk, loaded: true } as Clerk;
136+
clerkRef.value = loadedClerk;
137+
138+
await nextTick();
139+
140+
expect(callback).toHaveBeenCalledOnce();
141+
142+
// Simulate multiple updates (watcher should be cleaned up)
143+
for (let i = 0; i < 5; i++) {
144+
clerkRef.value = { ...mockClerk, loaded: true, session: { id: `sess_${i}` } } as Clerk;
145+
await nextTick();
146+
}
147+
148+
// Should still only be called once
149+
expect(callback).toHaveBeenCalledOnce();
150+
});
151+
});

packages/vue/src/utils/useClerkLoaded.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ import { useClerk } from '../composables';
1717
export const useClerkLoaded = (callback: (clerk: LoadedClerk) => void) => {
1818
const clerk = useClerk();
1919

20-
watch(
20+
let unwatch: (() => void) | undefined;
21+
// eslint-disable-next-line prefer-const
22+
unwatch = watch(
2123
clerk,
2224
unwrappedClerk => {
2325
if (!unwrappedClerk?.loaded) {
2426
return;
2527
}
2628

2729
callback(unwrappedClerk as LoadedClerk);
30+
unwatch?.();
2831
},
2932
{ immediate: true },
3033
);

0 commit comments

Comments
 (0)