Skip to content

Commit f39b2e1

Browse files
authored
fix(customize): debounce username preview requests (JhaSourav07#2452)
## Description Fixes JhaSourav07#2451 ### Summary This PR debounces username-based live preview requests in the customize dashboard to reduce unnecessary API calls while typing. ### Changes Made - Added a reusable `useDebounce` hook in `lib/hooks/useDebounce.ts` - Debounced username updates by 400ms before triggering preview requests - Generated a separate debounced preview query string for live preview requests - Kept export snippets and URL synchronization responsive and unchanged - Reused the existing `AbortController` implementation to cancel stale requests - Added unit tests for `useDebounce` to verify delayed execution behavior ### Result **Before** - A request was triggered on every username keystroke - Multiple in-flight requests could occur while typing **After** - Preview requests are only sent after the user pauses typing for 400ms - Stale requests are cancelled - Reduced API usage and improved preview stability ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A (behavioral improvement) ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [ ] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents 4d2e3db + d7ad995 commit f39b2e1

3 files changed

Lines changed: 113 additions & 2 deletions

File tree

app/customize/page.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
Language,
2121
Timezone,
2222
} from './types';
23+
import { useDebounce } from '@/hooks/useDebounce';
2324
import { getExportSnippet, buildQueryParams } from './utils';
2425

2526
// ─── Main Page ────────────────────────────────────────────────────────────────
@@ -54,6 +55,7 @@ function CustomizePageInner(): ReactElement {
5455
const [svgState, setSvgState] = useState<'idle' | 'loading' | 'loaded' | 'error'>('idle');
5556
const [errorMessage, setErrorMessage] = useState<string | null>(null);
5657
const trimmedUsername = username.trim();
58+
const debouncedUsername = useDebounce(trimmedUsername, 400);
5759
const hasUsername = trimmedUsername.length > 0;
5860
const isRandomTheme = theme === 'random';
5961

@@ -146,7 +148,32 @@ function CustomizePageInner(): ReactElement {
146148
language,
147149
timezone,
148150
});
149-
const previewSrc = `/api/streak?${queryString}`;
151+
152+
const previewQueryString = buildQueryParams({
153+
username: debouncedUsername,
154+
theme,
155+
bgHex,
156+
accentHex,
157+
textHex,
158+
scale,
159+
speed,
160+
font,
161+
year,
162+
radius,
163+
size,
164+
hideTitle,
165+
hideBackground,
166+
hideStats,
167+
viewMode,
168+
deltaFormat,
169+
badgeWidth,
170+
badgeHeight,
171+
grace,
172+
language,
173+
timezone,
174+
});
175+
176+
const previewSrc = `/api/streak?${previewQueryString}`;
150177

151178
// On change sync state to URL
152179
useEffect(() => {
@@ -169,6 +196,13 @@ function CustomizePageInner(): ReactElement {
169196
return;
170197
}
171198

199+
if (!validateGitHubUsername(debouncedUsername)) {
200+
setSvgContent('');
201+
setSvgState('error');
202+
setErrorMessage("That doesn't look like a valid GitHub username");
203+
return;
204+
}
205+
172206
setSvgState('loading');
173207
const controller = new AbortController();
174208

@@ -235,7 +269,7 @@ function CustomizePageInner(): ReactElement {
235269
});
236270

237271
return () => controller.abort();
238-
}, [previewSrc, hasUsername, trimmedUsername]);
272+
}, [previewSrc, hasUsername, debouncedUsername]);
239273

240274
const exportSnippet = getExportSnippet(exportFormat, queryString);
241275

lib/hooks/useDebounce.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3+
import { useDebounce } from './useDebounce';
4+
5+
describe('useDebounce', () => {
6+
beforeEach(() => {
7+
vi.useFakeTimers();
8+
});
9+
10+
afterEach(() => {
11+
vi.useRealTimers();
12+
});
13+
14+
it('updates only after the delay', () => {
15+
const { result, rerender } = renderHook(({ value }) => useDebounce(value, 400), {
16+
initialProps: { value: 'a' },
17+
});
18+
19+
expect(result.current).toBe('a');
20+
21+
rerender({ value: 'ab' });
22+
rerender({ value: 'abc' });
23+
24+
act(() => {
25+
vi.advanceTimersByTime(399);
26+
});
27+
28+
expect(result.current).toBe('a');
29+
30+
act(() => {
31+
vi.advanceTimersByTime(1);
32+
});
33+
34+
expect(result.current).toBe('abc');
35+
});
36+
37+
it('resets the timer when value changes repeatedly', () => {
38+
const { result, rerender } = renderHook(({ value }) => useDebounce(value, 400), {
39+
initialProps: { value: 'a' },
40+
});
41+
42+
rerender({ value: 'ab' });
43+
44+
act(() => {
45+
vi.advanceTimersByTime(200);
46+
});
47+
48+
rerender({ value: 'abc' });
49+
50+
act(() => {
51+
vi.advanceTimersByTime(200);
52+
});
53+
54+
expect(result.current).toBe('a');
55+
56+
act(() => {
57+
vi.advanceTimersByTime(200);
58+
});
59+
60+
expect(result.current).toBe('abc');
61+
});
62+
});

lib/hooks/useDebounce.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export function useDebounce<T>(value: T, delay: number) {
4+
const [debouncedValue, setDebouncedValue] = useState(value);
5+
6+
useEffect(() => {
7+
const timer = setTimeout(() => {
8+
setDebouncedValue(value);
9+
}, delay);
10+
11+
return () => clearTimeout(timer);
12+
}, [value, delay]);
13+
14+
return debouncedValue;
15+
}

0 commit comments

Comments
 (0)