Skip to content

Commit afc0a9e

Browse files
rsbhclaude
andcommitted
fix: search suggestions not loading on first open
Replace fumadocs useDocsSearch hook with custom fetch + lodash debounce. The fumadocs hook's useOnChange skips the initial render callback, so suggestions never load until user types and clears. Now fetches suggestions immediately when dialog opens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aab68a1 commit afc0a9e

1 file changed

Lines changed: 76 additions & 23 deletions

File tree

packages/chronicle/src/components/ui/search.tsx

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,86 @@ import {
44
MagnifyingGlassIcon
55
} from '@heroicons/react/24/outline';
66
import { Command, IconButton, Text } from '@raystack/apsara';
7-
import type { SortedResult } from 'fumadocs-core/search';
8-
import { useDocsSearch } from 'fumadocs-core/search/client';
9-
import { useCallback, useEffect, useState } from 'react';
7+
import debounce from 'lodash/debounce';
8+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
109
import { useNavigate } from 'react-router';
1110
import { MethodBadge } from '@/components/api/method-badge';
1211
import { usePageContext } from '@/lib/page-context';
1312
import styles from './search.module.css';
1413

14+
interface SearchResult {
15+
id: string;
16+
url: string;
17+
type: string;
18+
content: string;
19+
}
20+
1521
interface SearchProps {
1622
classNames?: { trigger?: string };
1723
}
1824

25+
function buildSearchUrl(query: string, tag?: string): string {
26+
const params = new URLSearchParams();
27+
if (query) params.set('query', query);
28+
if (tag) params.set('tag', tag);
29+
const qs = params.toString();
30+
return qs ? `/api/search?${qs}` : '/api/search';
31+
}
32+
1933
export function Search({ classNames }: SearchProps) {
2034
const [open, setOpen] = useState(false);
35+
const [search, setSearch] = useState('');
36+
const [results, setResults] = useState<SearchResult[]>([]);
37+
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
38+
const [isLoading, setIsLoading] = useState(false);
2139
const navigate = useNavigate();
2240
const { version } = usePageContext();
41+
const tag = version.dir ?? undefined;
42+
const abortRef = useRef<AbortController | null>(null);
2343

24-
const { search, setSearch, query } = useDocsSearch({
25-
type: 'fetch',
26-
api: '/api/search',
27-
tag: version.dir ?? undefined,
28-
delayMs: 100,
29-
allowEmpty: true
30-
});
44+
const fetchResults = useCallback(async (query: string, signal?: AbortSignal) => {
45+
setIsLoading(true);
46+
try {
47+
const res = await fetch(buildSearchUrl(query, tag), { signal });
48+
if (!res.ok || signal?.aborted) return;
49+
const data: SearchResult[] = await res.json();
50+
if (signal?.aborted) return;
51+
if (query) {
52+
setResults(data);
53+
} else {
54+
setSuggestions(data);
55+
}
56+
} catch (err) {
57+
if (err instanceof DOMException && err.name === 'AbortError') return;
58+
console.error('Search fetch failed:', err);
59+
} finally {
60+
setIsLoading(false);
61+
}
62+
}, [tag]);
63+
64+
const debouncedSearch = useMemo(
65+
() => debounce((query: string) => {
66+
abortRef.current?.abort();
67+
const controller = new AbortController();
68+
abortRef.current = controller;
69+
fetchResults(query, controller.signal);
70+
}, 150),
71+
[fetchResults]
72+
);
73+
74+
useEffect(() => {
75+
if (!open) {
76+
setSearch('');
77+
setResults([]);
78+
return;
79+
}
80+
if (!search) {
81+
fetchResults('');
82+
return;
83+
}
84+
debouncedSearch(search);
85+
return () => debouncedSearch.cancel();
86+
}, [open, search, fetchResults, debouncedSearch]);
3187

3288
const onSelect = useCallback(
3389
(url: string) => {
@@ -49,9 +105,7 @@ export function Search({ classNames }: SearchProps) {
49105
return () => document.removeEventListener('keydown', down);
50106
}, []);
51107

52-
const results = deduplicateByUrl(
53-
query.data === 'empty' ? [] : (query.data ?? [])
54-
);
108+
const displayResults = deduplicateByUrl(search ? results : suggestions);
55109

56110
return (
57111
<>
@@ -77,18 +131,17 @@ export function Search({ classNames }: SearchProps) {
77131
/>
78132

79133
<Command.Content className={styles.list}>
80-
{query.isLoading && <Command.Empty>Loading...</Command.Empty>}
81-
{!query.isLoading &&
134+
{isLoading && displayResults.length === 0 && <Command.Empty>Loading...</Command.Empty>}
135+
{!isLoading &&
82136
search.length > 0 &&
83-
results.length === 0 && (
137+
displayResults.length === 0 && (
84138
<Command.Empty>No results found.</Command.Empty>
85139
)}
86-
{!query.isLoading &&
87-
search.length === 0 &&
88-
results.length > 0 && (
140+
{search.length === 0 &&
141+
displayResults.length > 0 && (
89142
<Command.Group>
90143
<Command.Label>Suggestions</Command.Label>
91-
{results.slice(0, 8).map((result: SortedResult) => (
144+
{displayResults.slice(0, 8).map((result) => (
92145
<Command.Item
93146
key={result.id}
94147
value={result.id}
@@ -108,7 +161,7 @@ export function Search({ classNames }: SearchProps) {
108161
</Command.Group>
109162
)}
110163
{search.length > 0 &&
111-
results.map((result: SortedResult) => (
164+
displayResults.map((result) => (
112165
<Command.Item
113166
key={result.id}
114167
value={result.id}
@@ -149,7 +202,7 @@ export function Search({ classNames }: SearchProps) {
149202
);
150203
}
151204

152-
function deduplicateByUrl(results: SortedResult[]): SortedResult[] {
205+
function deduplicateByUrl(results: SearchResult[]): SearchResult[] {
153206
const seen = new Set<string>();
154207
return results.filter(r => {
155208
const base = r.url.split('#')[0];
@@ -183,7 +236,7 @@ function HighlightedText({
183236
);
184237
}
185238

186-
function getResultIcon(result: SortedResult): React.ReactNode {
239+
function getResultIcon(result: SearchResult): React.ReactNode {
187240
if (!result.url.startsWith('/apis/')) {
188241
return result.type === 'page' ? (
189242
<DocumentIcon className={styles.icon} />

0 commit comments

Comments
 (0)