Skip to content

Commit ef89195

Browse files
nullcoderclaude
andcommitted
refactor: use official onloadTurnstileCallback instead of polling
- Replace setInterval polling with Cloudflare's official onload callback mechanism - Add onloadTurnstileCallback to Window interface - Update script URL to include onload parameter - Fix bug where cleanup function wasn't registered when Turnstile was pre-loaded - Update tests to properly simulate the callback flow - Better performance and cleaner implementation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4b1b236 commit ef89195

File tree

2 files changed

+54
-28
lines changed

2 files changed

+54
-28
lines changed

components/ui/turnstile.test.tsx

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ describe("Turnstile", () => {
2727
});
2828

2929
it("renders turnstile widget when script loads", async () => {
30+
// Temporarily remove window.turnstile to simulate script not loaded yet
31+
const originalTurnstile = window.turnstile;
32+
delete (window as any).turnstile;
33+
3034
const { container } = render(
3135
<Turnstile
3236
sitekey="test-site-key"
@@ -36,7 +40,11 @@ describe("Turnstile", () => {
3640
/>
3741
);
3842

39-
// Wait for script to load and widget to render
43+
// Restore window.turnstile and call the callback
44+
window.turnstile = originalTurnstile;
45+
window.onloadTurnstileCallback?.();
46+
47+
// Wait for widget to render
4048
await waitFor(() => {
4149
expect(mockRender).toHaveBeenCalledWith(
4250
expect.any(HTMLElement),
@@ -95,28 +103,49 @@ describe("Turnstile", () => {
95103
});
96104
});
97105

98-
it("cleans up widget on unmount", async () => {
106+
it("cleans up widget on unmount", () => {
99107
const { unmount } = render(
100108
<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />
101109
);
102110

103-
await waitFor(() => {
104-
expect(mockRender).toHaveBeenCalled();
105-
});
111+
// Widget should render immediately since window.turnstile exists
112+
expect(mockRender).toHaveBeenCalled();
113+
expect(mockRender).toHaveReturnedWith("widget-123");
114+
115+
// Clear all mocks to ensure clean state for testing cleanup
116+
mockRender.mockClear();
106117

107118
unmount();
108119

109120
expect(mockRemove).toHaveBeenCalledWith("widget-123");
110121
});
111122

112-
it("checks script is loaded", async () => {
123+
it("renders immediately when turnstile is already loaded", () => {
124+
// Turnstile is already set up in beforeEach
125+
render(<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />);
126+
127+
// Should render immediately without needing the callback
128+
expect(mockRender).toHaveBeenCalled();
129+
});
130+
131+
it("checks script is loaded with correct parameters", async () => {
132+
// Remove existing script if any
133+
const existingScript = document.getElementById("cf-turnstile-script");
134+
existingScript?.remove();
135+
136+
// Temporarily remove window.turnstile
137+
delete (window as any).turnstile;
138+
113139
render(<Turnstile sitekey="test-site-key" onSuccess={mockOnSuccess} />);
114140

115141
// Check that script was added
116142
const script = document.getElementById("cf-turnstile-script");
117143
expect(script).toBeTruthy();
118144
expect(script?.getAttribute("src")).toBe(
119-
"https://challenges.cloudflare.com/turnstile/v0/api.js"
145+
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback"
120146
);
147+
148+
// Check that callback was set
149+
expect(window.onloadTurnstileCallback).toBeDefined();
121150
});
122151
});

components/ui/turnstile.tsx

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ declare global {
3737
remove: (widgetId: string) => void;
3838
execute: (widgetId: string) => void;
3939
};
40+
onloadTurnstileCallback?: () => void;
4041
}
4142
}
4243

@@ -56,18 +57,6 @@ const Turnstile: React.FC<TurnstileProps> = ({
5657
const widgetIdRef = useRef<string | null>(null);
5758

5859
useEffect(() => {
59-
// Load Turnstile script if not already present
60-
const scriptId = "cf-turnstile-script";
61-
if (!document.getElementById(scriptId)) {
62-
const script = document.createElement("script");
63-
script.id = scriptId;
64-
script.src =
65-
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
66-
script.async = true;
67-
script.defer = true;
68-
document.head.appendChild(script);
69-
}
70-
7160
const renderWidget = () => {
7261
if (window.turnstile && containerRef.current && !widgetIdRef.current) {
7362
widgetIdRef.current = window.turnstile.render(containerRef.current, {
@@ -85,27 +74,35 @@ const Turnstile: React.FC<TurnstileProps> = ({
8574
}
8675
};
8776

88-
// Render after script loads
77+
// Check if Turnstile is already loaded
8978
if (window.turnstile) {
9079
renderWidget();
9180
} else {
92-
const interval = setInterval(() => {
93-
if (window.turnstile) {
94-
clearInterval(interval);
95-
renderWidget();
96-
}
97-
}, 100);
98-
return () => clearInterval(interval);
81+
// Set up the callback for when Turnstile loads
82+
window.onloadTurnstileCallback = renderWidget;
83+
84+
// Load Turnstile script if not already present
85+
const scriptId = "cf-turnstile-script";
86+
if (!document.getElementById(scriptId)) {
87+
const script = document.createElement("script");
88+
script.id = scriptId;
89+
script.src =
90+
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback";
91+
script.async = true;
92+
script.defer = true;
93+
document.head.appendChild(script);
94+
}
9995
}
10096

101-
// Cleanup
97+
// Cleanup - always register cleanup function
10298
return () => {
10399
if (widgetIdRef.current && window.turnstile) {
104100
try {
105101
window.turnstile.remove(widgetIdRef.current);
106102
} catch {
107103
// Widget might already be removed
108104
}
105+
widgetIdRef.current = null;
109106
}
110107
};
111108
}, [

0 commit comments

Comments
 (0)