From afc0a9efe77af0c0e2964166e7c593e092977ad7 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 7 May 2026 11:24:13 +0530 Subject: [PATCH] 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) --- .../chronicle/src/components/ui/search.tsx | 99 ++++++++++++++----- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index 1a313ac4..f5c8bd62 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -4,30 +4,86 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { Command, IconButton, Text } from '@raystack/apsara'; -import type { SortedResult } from 'fumadocs-core/search'; -import { useDocsSearch } from 'fumadocs-core/search/client'; -import { useCallback, useEffect, useState } from 'react'; +import debounce from 'lodash/debounce'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; import { MethodBadge } from '@/components/api/method-badge'; import { usePageContext } from '@/lib/page-context'; import styles from './search.module.css'; +interface SearchResult { + id: string; + url: string; + type: string; + content: string; +} + interface SearchProps { classNames?: { trigger?: string }; } +function buildSearchUrl(query: string, tag?: string): string { + const params = new URLSearchParams(); + if (query) params.set('query', query); + if (tag) params.set('tag', tag); + const qs = params.toString(); + return qs ? `/api/search?${qs}` : '/api/search'; +} + export function Search({ classNames }: SearchProps) { const [open, setOpen] = useState(false); + const [search, setSearch] = useState(''); + const [results, setResults] = useState([]); + const [suggestions, setSuggestions] = useState([]); + const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const { version } = usePageContext(); + const tag = version.dir ?? undefined; + const abortRef = useRef(null); - const { search, setSearch, query } = useDocsSearch({ - type: 'fetch', - api: '/api/search', - tag: version.dir ?? undefined, - delayMs: 100, - allowEmpty: true - }); + const fetchResults = useCallback(async (query: string, signal?: AbortSignal) => { + setIsLoading(true); + try { + const res = await fetch(buildSearchUrl(query, tag), { signal }); + if (!res.ok || signal?.aborted) return; + const data: SearchResult[] = await res.json(); + if (signal?.aborted) return; + if (query) { + setResults(data); + } else { + setSuggestions(data); + } + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') return; + console.error('Search fetch failed:', err); + } finally { + setIsLoading(false); + } + }, [tag]); + + const debouncedSearch = useMemo( + () => debounce((query: string) => { + abortRef.current?.abort(); + const controller = new AbortController(); + abortRef.current = controller; + fetchResults(query, controller.signal); + }, 150), + [fetchResults] + ); + + useEffect(() => { + if (!open) { + setSearch(''); + setResults([]); + return; + } + if (!search) { + fetchResults(''); + return; + } + debouncedSearch(search); + return () => debouncedSearch.cancel(); + }, [open, search, fetchResults, debouncedSearch]); const onSelect = useCallback( (url: string) => { @@ -49,9 +105,7 @@ export function Search({ classNames }: SearchProps) { return () => document.removeEventListener('keydown', down); }, []); - const results = deduplicateByUrl( - query.data === 'empty' ? [] : (query.data ?? []) - ); + const displayResults = deduplicateByUrl(search ? results : suggestions); return ( <> @@ -77,18 +131,17 @@ export function Search({ classNames }: SearchProps) { /> - {query.isLoading && Loading...} - {!query.isLoading && + {isLoading && displayResults.length === 0 && Loading...} + {!isLoading && search.length > 0 && - results.length === 0 && ( + displayResults.length === 0 && ( No results found. )} - {!query.isLoading && - search.length === 0 && - results.length > 0 && ( + {search.length === 0 && + displayResults.length > 0 && ( Suggestions - {results.slice(0, 8).map((result: SortedResult) => ( + {displayResults.slice(0, 8).map((result) => ( )} {search.length > 0 && - results.map((result: SortedResult) => ( + displayResults.map((result) => ( (); return results.filter(r => { const base = r.url.split('#')[0]; @@ -183,7 +236,7 @@ function HighlightedText({ ); } -function getResultIcon(result: SortedResult): React.ReactNode { +function getResultIcon(result: SearchResult): React.ReactNode { if (!result.url.startsWith('/apis/')) { return result.type === 'page' ? (