Skip to content

Commit 719c01b

Browse files
committed
fix(oauth): prevent double-execution and memory leaks in callback page
- Add callbackRanRef to prevent handleCallback running twice in StrictMode - Add mounted flag to prevent state updates after unmount - Store timeout in ref and clear on cleanup to prevent memory leak
1 parent ecf37db commit 719c01b

1 file changed

Lines changed: 31 additions & 8 deletions

File tree

frontend/src/pages/GitHubCallbackPage.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useEffect, useState, useRef } from 'react';
22
import { useNavigate, useSearchParams } from 'react-router-dom';
33
import { Loader2, CheckCircle2, XCircle } from 'lucide-react';
44
import { useGitHubRepos } from '@/hooks/useGitHubRepos';
@@ -9,8 +9,17 @@ export function GitHubCallbackPage() {
99
const { completeConnect } = useGitHubRepos();
1010
const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing');
1111
const [errorMessage, setErrorMessage] = useState<string>('');
12+
13+
const callbackRanRef = useRef(false);
14+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
1215

1316
useEffect(() => {
17+
// Prevent double-execution in React StrictMode
18+
if (callbackRanRef.current) return;
19+
callbackRanRef.current = true;
20+
21+
let mounted = true;
22+
1423
const handleCallback = async () => {
1524
const code = searchParams.get('code');
1625
const state = searchParams.get('state');
@@ -19,25 +28,32 @@ export function GitHubCallbackPage() {
1928

2029
// Handle GitHub OAuth errors
2130
if (error) {
22-
setStatus('error');
23-
setErrorMessage(errorDescription || error || 'GitHub authorization failed');
31+
if (mounted) {
32+
setStatus('error');
33+
setErrorMessage(errorDescription || error || 'GitHub authorization failed');
34+
}
2435
return;
2536
}
2637

2738
if (!code || !state) {
28-
setStatus('error');
29-
setErrorMessage('Missing authorization code or state');
39+
if (mounted) {
40+
setStatus('error');
41+
setErrorMessage('Missing authorization code or state');
42+
}
3043
return;
3144
}
3245

3346
// Exchange code for token via backend
3447
const success = await completeConnect(code, state);
3548

49+
if (!mounted) return;
50+
3651
if (success) {
3752
setStatus('success');
38-
// Redirect to dashboard after brief success message
39-
setTimeout(() => {
40-
navigate('/dashboard', { replace: true });
53+
timeoutRef.current = setTimeout(() => {
54+
if (mounted) {
55+
navigate('/dashboard', { replace: true });
56+
}
4157
}, 1500);
4258
} else {
4359
setStatus('error');
@@ -46,6 +62,13 @@ export function GitHubCallbackPage() {
4662
};
4763

4864
handleCallback();
65+
66+
return () => {
67+
mounted = false;
68+
if (timeoutRef.current) {
69+
clearTimeout(timeoutRef.current);
70+
}
71+
};
4972
}, [searchParams, completeConnect, navigate]);
5073

5174
return (

0 commit comments

Comments
 (0)