Skip to content

Commit 0c7db9a

Browse files
committed
fix(web): resolve search page crash with TanStack Table hooks
Replace BaseTable with Mantine Table on search page to fix "Invalid hook call" error. TanStack Table's useReactTable and useVirtualizer hooks are incompatible with TanStack Router's lazy-loaded routes, causing React's dispatcher to be null when hooks are called. The search results now use Mantine Table directly with: - Clickable entity name links - Entity type badges with canonical colors - Citation and work counts - Hint text displayed under entity names This is a targeted fix for the search page only; BaseTable continues to work correctly in non-lazy-loaded contexts.
1 parent 72d24ba commit 0c7db9a

File tree

1 file changed

+59
-130
lines changed

1 file changed

+59
-130
lines changed

apps/web/src/routes/search.lazy.tsx

Lines changed: 59 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Container,
1212
Group,
1313
Stack,
14+
Table,
1415
Text,
1516
Title,
1617
} from "@mantine/core";
@@ -21,16 +22,13 @@ import {
2122
} from "@tabler/icons-react";
2223
import { useQuery, useQueryClient } from "@tanstack/react-query";
2324
import { createLazyFileRoute,useSearch } from "@tanstack/react-router";
24-
import type { ColumnDef } from "@tanstack/react-table";
2525
import { useEffect, useMemo, useState } from "react";
2626

2727
import { BORDER_STYLE_GRAY_3, ICON_SIZE, SEARCH, TIME_MS } from '@/config/style-constants';
2828
import { useUserInteractions } from "@/hooks/use-user-interactions";
2929

3030
import { SearchInterface } from "../components/search/SearchInterface";
31-
import { SearchResultPreview, useSearchResultHover } from "../components/search/SearchResultPreview";
3231
import { SearchResultsSkeleton } from "../components/search/SearchResultsSkeleton";
33-
import { BaseTable } from "../components/tables/BaseTable";
3432
import { pageDescription, pageTitle } from "../styles/layout.css";
3533

3634
interface SearchFilters {
@@ -133,113 +131,6 @@ const getEntityTypeColor = (entityType: AutocompleteResult["entity_type"]) => {
133131
return "gray";
134132
};
135133

136-
/**
137-
* Name cell component - extracted to use hooks properly
138-
* React hooks must be called at the top level of a component, not in render functions
139-
*/
140-
const SearchResultNameCell = ({ result }: { result: AutocompleteResult }) => {
141-
const entityUrl = convertToRelativeUrl(result.id);
142-
const hover = useSearchResultHover(result);
143-
144-
return (
145-
<>
146-
<div {...hover.props}>
147-
{entityUrl ? (
148-
<Anchor
149-
href={entityUrl}
150-
size="sm"
151-
fw={500}
152-
style={{ textDecoration: "none" }}
153-
aria-label={`View ${result.entity_type} ${result.display_name}`}
154-
>
155-
{result.display_name}
156-
</Anchor>
157-
) : (
158-
<Text fw={500} size="sm">
159-
{result.display_name}
160-
</Text>
161-
)}
162-
{result.hint && (
163-
<Text size="xs" c="dimmed" lineClamp={1}>
164-
{result.hint}
165-
</Text>
166-
)}
167-
{result.external_id && (
168-
<Text size="xs" c="dimmed">
169-
{result.external_id}
170-
</Text>
171-
)}
172-
</div>
173-
174-
{/* Hover preview card */}
175-
<SearchResultPreview
176-
entity={result}
177-
opened={hover.opened}
178-
onToggle={hover.toggle}
179-
targetElement={hover.targetElement}
180-
/>
181-
</>
182-
);
183-
};
184-
185-
// Extract column definitions to reduce complexity
186-
const createSearchColumns = (): ColumnDef<AutocompleteResult>[] => [
187-
{
188-
accessorKey: "entity_type",
189-
header: "Type",
190-
size: 100,
191-
cell: ({ row }) => {
192-
const result = row.original;
193-
return (
194-
<Badge
195-
size="sm"
196-
color={getEntityTypeColor(result.entity_type)}
197-
variant="light"
198-
>
199-
{result.entity_type}
200-
</Badge>
201-
);
202-
},
203-
},
204-
{
205-
accessorKey: "display_name",
206-
header: "Name",
207-
cell: ({ row }) => <SearchResultNameCell result={row.original} />,
208-
},
209-
{
210-
accessorKey: "cited_by_count",
211-
header: "Citations",
212-
size: 120,
213-
cell: ({ row }) => {
214-
const count = row.original.cited_by_count;
215-
return count ? (
216-
<Text size="sm" fw={500}>
217-
{formatLargeNumber(count)}
218-
</Text>
219-
) : (
220-
<Text size="sm" c="dimmed">
221-
222-
</Text>
223-
);
224-
},
225-
},
226-
{
227-
accessorKey: "works_count",
228-
header: "Works",
229-
size: 100,
230-
cell: ({ row }) => {
231-
const count = row.original.works_count;
232-
return count ? (
233-
<Text size="sm">{formatLargeNumber(count)}</Text>
234-
) : (
235-
<Text size="sm" c="dimmed">
236-
237-
</Text>
238-
);
239-
},
240-
},
241-
];
242-
243134
const SearchPage = () => {
244135
const searchParams = useSearch({ from: "/search" });
245136
const queryClient = useQueryClient();
@@ -293,8 +184,6 @@ const SearchPage = () => {
293184
staleTime: TIME_MS.SEARCH_STALE_TIME,
294185
});
295186

296-
const columns = createSearchColumns();
297-
298187
const handleSearch = async (filters: SearchFilters) => {
299188
setSearchFilters(filters);
300189
// Auto-tracking in useUserInteractions will handle page visit recording
@@ -451,24 +340,64 @@ const SearchPage = () => {
451340
)}
452341
</Group>
453342

454-
<BaseTable
455-
data={searchResults}
456-
columns={columns}
457-
searchable={false} // Search is handled by the SearchInterface
458-
onRowClick={(result) => {
459-
logger.debug(
460-
"ui",
461-
"Search result clicked",
462-
{
463-
id: result.id,
464-
name: result.display_name,
465-
type: result.entity_type,
466-
},
467-
"SearchPage",
468-
);
469-
// Navigation is handled by the entity links in the table
470-
}}
471-
/>
343+
{/* Using Mantine Table directly due to TanStack Table hook compatibility
344+
issues with lazy-loaded routes. BaseTable's useReactTable/useVirtualizer
345+
hooks cause "Invalid hook call" errors in this lazy route context. */}
346+
<Table striped highlightOnHover withTableBorder>
347+
<Table.Thead>
348+
<Table.Tr>
349+
<Table.Th w={100}>Type</Table.Th>
350+
<Table.Th>Name</Table.Th>
351+
<Table.Th w={100}>Citations</Table.Th>
352+
<Table.Th w={100}>Works</Table.Th>
353+
</Table.Tr>
354+
</Table.Thead>
355+
<Table.Tbody>
356+
{searchResults.map((result) => {
357+
const entityUrl = convertToRelativeUrl(result.id);
358+
return (
359+
<Table.Tr key={result.id}>
360+
<Table.Td>
361+
<Badge size="sm" color={getEntityTypeColor(result.entity_type)} variant="light">
362+
{result.entity_type}
363+
</Badge>
364+
</Table.Td>
365+
<Table.Td>
366+
<Stack gap={2}>
367+
{entityUrl ? (
368+
<Anchor
369+
href={entityUrl}
370+
size="sm"
371+
fw={500}
372+
style={{ textDecoration: "none" }}
373+
>
374+
{result.display_name}
375+
</Anchor>
376+
) : (
377+
<Text size="sm" fw={500}>{result.display_name}</Text>
378+
)}
379+
{result.hint && (
380+
<Text size="xs" c="dimmed" lineClamp={1}>
381+
{result.hint}
382+
</Text>
383+
)}
384+
</Stack>
385+
</Table.Td>
386+
<Table.Td>
387+
<Text size="sm" fw={500}>
388+
{result.cited_by_count ? formatLargeNumber(result.cited_by_count) : '—'}
389+
</Text>
390+
</Table.Td>
391+
<Table.Td>
392+
<Text size="sm">
393+
{result.works_count ? formatLargeNumber(result.works_count) : '—'}
394+
</Text>
395+
</Table.Td>
396+
</Table.Tr>
397+
);
398+
})}
399+
</Table.Tbody>
400+
</Table>
472401
</Stack>
473402
);
474403
};

0 commit comments

Comments
 (0)