From 921e0a0de619cce6075c4a59a31ef7f15f609f5d Mon Sep 17 00:00:00 2001 From: ephraimduncan Date: Mon, 16 Feb 2026 20:25:03 +0000 Subject: [PATCH] perf: reduce rerenders and async waterfalls in document tables Parallelize user + team lookups in findDocuments via Promise.all, dedupe redundant getTeamById call in the internal TRPC route, stabilize useUpdateSearchParams callback with useCallback + refs, memoize parsed search params in toolbar components, fix stale columns memo deps in template documents table, and use replace:true for search/filter URL updates to avoid history spam. --- .../template-page-view-documents-table.tsx | 9 ++-- .../tables/documents-table-toolbar.tsx | 48 ++++++++++++------- .../template-documents-table-toolbar.tsx | 48 ++++++++++++------- .../tables/templates-table-toolbar.tsx | 24 ++++++---- .../hooks/use-update-search-params.ts | 39 +++++++++------ .../server-only/document/find-documents.ts | 34 ++++++------- .../find-documents-internal.ts | 5 +- 7 files changed, 128 insertions(+), 79 deletions(-) diff --git a/apps/remix/app/components/general/template/template-page-view-documents-table.tsx b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx index 9974cb5a4..e4a1fc048 100644 --- a/apps/remix/app/components/general/template/template-page-view-documents-table.tsx +++ b/apps/remix/app/components/general/template/template-page-view-documents-table.tsx @@ -56,8 +56,11 @@ export const TemplatePageViewDocumentsTable = ({ const team = useCurrentTeam(); - const parsedSearchParams = ZDocumentSearchParamsSchema.parse( - Object.fromEntries(searchParams ?? []), + const searchParamsString = searchParams.toString(); + + const parsedSearchParams = useMemo( + () => ZDocumentSearchParamsSchema.parse(Object.fromEntries(searchParams)), + [searchParamsString], ); const { data, isLoading, isLoadingError } = trpc.document.findDocumentsInternal.useQuery( @@ -174,7 +177,7 @@ export const TemplatePageViewDocumentsTable = ({ ), }, ] satisfies DataTableColumnDef<(typeof results)['data'][number]>[]; - }, []); + }, [_, i18n, team?.url]); return (
diff --git a/apps/remix/app/components/tables/documents-table-toolbar.tsx b/apps/remix/app/components/tables/documents-table-toolbar.tsx index fd9380a4e..47c9d0906 100644 --- a/apps/remix/app/components/tables/documents-table-toolbar.tsx +++ b/apps/remix/app/components/tables/documents-table-toolbar.tsx @@ -36,9 +36,11 @@ export const DocumentsTableToolbar = ({ const query = searchParams.get('query') ?? ''; const period = searchParams.get('period') ?? ''; + const statusParam = searchParams.get('status'); + const senderIdsParam = searchParams.get('senderIds'); - const selectedStatusValues = parseToStringArray(searchParams.get('status')); - const selectedSenderValues = parseToStringArray(searchParams.get('senderIds')); + const selectedStatusValues = useMemo(() => parseToStringArray(statusParam), [statusParam]); + const selectedSenderValues = useMemo(() => parseToStringArray(senderIdsParam), [senderIdsParam]); const [searchTerm, setSearchTerm] = useState(query); const debouncedSearchTerm = useDebouncedValue(searchTerm, 500); @@ -56,8 +58,11 @@ export const DocumentsTableToolbar = ({ return; } - updateSearchParams({ query: debouncedSearchTerm || undefined, page: undefined }); - }, [debouncedSearchTerm, query, searchTerm]); + updateSearchParams( + { query: debouncedSearchTerm || undefined, page: undefined }, + { replace: true }, + ); + }, [debouncedSearchTerm, query, searchTerm, updateSearchParams]); const { data: members } = trpc.team.member.getMany.useQuery( { @@ -118,7 +123,7 @@ export const DocumentsTableToolbar = ({ className="absolute inset-y-0 end-0 flex w-9 items-center justify-center text-muted-foreground hover:text-foreground" onClick={() => { setSearchTerm(''); - updateSearchParams({ query: undefined, page: undefined }); + updateSearchParams({ query: undefined, page: undefined }, { replace: true }); }} > @@ -133,10 +138,13 @@ export const DocumentsTableToolbar = ({ counts={statusCounts} showSearch={false} onSelectedValuesChange={(values) => { - updateSearchParams({ - status: toCommaSeparatedSearchParam(values), - page: undefined, - }); + updateSearchParams( + { + status: toCommaSeparatedSearchParam(values), + page: undefined, + }, + { replace: true }, + ); }} /> @@ -147,10 +155,13 @@ export const DocumentsTableToolbar = ({ selectedValues={selectedSenderValues} showSearch onSelectedValuesChange={(values) => { - updateSearchParams({ - senderIds: toCommaSeparatedSearchParam(values), - page: undefined, - }); + updateSearchParams( + { + senderIds: toCommaSeparatedSearchParam(values), + page: undefined, + }, + { replace: true }, + ); }} /> )} @@ -164,10 +175,13 @@ export const DocumentsTableToolbar = ({ onSelectedValuesChange={(values) => { const nextPeriod = values[0]; - updateSearchParams({ - period: nextPeriod ?? undefined, - page: undefined, - }); + updateSearchParams( + { + period: nextPeriod ?? undefined, + page: undefined, + }, + { replace: true }, + ); }} /> diff --git a/apps/remix/app/components/tables/template-documents-table-toolbar.tsx b/apps/remix/app/components/tables/template-documents-table-toolbar.tsx index b68d3e014..f79db8e3c 100644 --- a/apps/remix/app/components/tables/template-documents-table-toolbar.tsx +++ b/apps/remix/app/components/tables/template-documents-table-toolbar.tsx @@ -25,9 +25,11 @@ export const TemplateDocumentsTableToolbar = () => { const query = searchParams.get('query') ?? ''; const period = searchParams.get('period') ?? ''; + const statusParam = searchParams.get('status'); + const sourceParam = searchParams.get('source'); - const selectedStatusValues = parseToStringArray(searchParams.get('status')); - const selectedSourceValues = parseToStringArray(searchParams.get('source')); + const selectedStatusValues = useMemo(() => parseToStringArray(statusParam), [statusParam]); + const selectedSourceValues = useMemo(() => parseToStringArray(sourceParam), [sourceParam]); const [searchTerm, setSearchTerm] = useState(query); const debouncedSearchTerm = useDebouncedValue(searchTerm, 500); @@ -45,8 +47,11 @@ export const TemplateDocumentsTableToolbar = () => { return; } - updateSearchParams({ query: debouncedSearchTerm || undefined, page: undefined }); - }, [debouncedSearchTerm, query, searchTerm]); + updateSearchParams( + { query: debouncedSearchTerm || undefined, page: undefined }, + { replace: true }, + ); + }, [debouncedSearchTerm, query, searchTerm, updateSearchParams]); const statusOptions = useMemo( () => [ @@ -131,7 +136,7 @@ export const TemplateDocumentsTableToolbar = () => { className="absolute inset-y-0 end-0 flex w-9 items-center justify-center text-muted-foreground hover:text-foreground" onClick={() => { setSearchTerm(''); - updateSearchParams({ query: undefined, page: undefined }); + updateSearchParams({ query: undefined, page: undefined }, { replace: true }); }} > @@ -145,10 +150,13 @@ export const TemplateDocumentsTableToolbar = () => { selectedValues={selectedStatusValues} showSearch={false} onSelectedValuesChange={(values) => { - updateSearchParams({ - status: toCommaSeparatedSearchParam(values), - page: undefined, - }); + updateSearchParams( + { + status: toCommaSeparatedSearchParam(values), + page: undefined, + }, + { replace: true }, + ); }} /> @@ -158,10 +166,13 @@ export const TemplateDocumentsTableToolbar = () => { selectedValues={selectedSourceValues} showSearch={false} onSelectedValuesChange={(values) => { - updateSearchParams({ - source: toCommaSeparatedSearchParam(values), - page: undefined, - }); + updateSearchParams( + { + source: toCommaSeparatedSearchParam(values), + page: undefined, + }, + { replace: true }, + ); }} /> @@ -174,10 +185,13 @@ export const TemplateDocumentsTableToolbar = () => { onSelectedValuesChange={(values) => { const nextPeriod = values[0]; - updateSearchParams({ - period: nextPeriod ?? undefined, - page: undefined, - }); + updateSearchParams( + { + period: nextPeriod ?? undefined, + page: undefined, + }, + { replace: true }, + ); }} /> diff --git a/apps/remix/app/components/tables/templates-table-toolbar.tsx b/apps/remix/app/components/tables/templates-table-toolbar.tsx index d872f1c8b..048476880 100644 --- a/apps/remix/app/components/tables/templates-table-toolbar.tsx +++ b/apps/remix/app/components/tables/templates-table-toolbar.tsx @@ -22,7 +22,9 @@ export const TemplatesTableToolbar = () => { const updateSearchParams = useUpdateSearchParams(); const query = searchParams.get('query') ?? ''; - const selectedTypeValues = parseToStringArray(searchParams.get('type')); + const typeParam = searchParams.get('type'); + + const selectedTypeValues = useMemo(() => parseToStringArray(typeParam), [typeParam]); const [searchTerm, setSearchTerm] = useState(query); const debouncedSearchTerm = useDebouncedValue(searchTerm, 500); @@ -40,8 +42,11 @@ export const TemplatesTableToolbar = () => { return; } - updateSearchParams({ query: debouncedSearchTerm || undefined, page: undefined }); - }, [debouncedSearchTerm, query, searchTerm]); + updateSearchParams( + { query: debouncedSearchTerm || undefined, page: undefined }, + { replace: true }, + ); + }, [debouncedSearchTerm, query, searchTerm, updateSearchParams]); const typeOptions = useMemo( () => [ @@ -91,7 +96,7 @@ export const TemplatesTableToolbar = () => { className="absolute inset-y-0 end-0 flex w-9 items-center justify-center text-muted-foreground hover:text-foreground" onClick={() => { setSearchTerm(''); - updateSearchParams({ query: undefined, page: undefined }); + updateSearchParams({ query: undefined, page: undefined }, { replace: true }); }} > @@ -105,10 +110,13 @@ export const TemplatesTableToolbar = () => { selectedValues={selectedTypeValues} showSearch={false} onSelectedValuesChange={(values) => { - updateSearchParams({ - type: toCommaSeparatedSearchParam(values), - page: undefined, - }); + updateSearchParams( + { + type: toCommaSeparatedSearchParam(values), + page: undefined, + }, + { replace: true }, + ); }} /> diff --git a/packages/lib/client-only/hooks/use-update-search-params.ts b/packages/lib/client-only/hooks/use-update-search-params.ts index 0f59ed6fa..5983db085 100644 --- a/packages/lib/client-only/hooks/use-update-search-params.ts +++ b/packages/lib/client-only/hooks/use-update-search-params.ts @@ -1,3 +1,5 @@ +import { useCallback, useRef } from 'react'; + import type { NavigateOptions } from 'react-router'; import { useSearchParams } from 'react-router'; @@ -7,20 +9,29 @@ type UpdateSearchParamsOptions = Pick { const [searchParams, setSearchParams] = useSearchParams(); - return (params: SearchParamValues, options?: UpdateSearchParamsOptions) => { - const nextSearchParams = new URLSearchParams(searchParams?.toString() ?? ''); + const searchParamsRef = useRef(searchParams); + searchParamsRef.current = searchParams; - Object.entries(params).forEach(([key, value]) => { - if (value === undefined || value === null) { - nextSearchParams.delete(key); - } else { - nextSearchParams.set(key, String(value)); - } - }); + const defaultOptionsRef = useRef(defaultOptions); + defaultOptionsRef.current = defaultOptions; - setSearchParams(nextSearchParams, { - ...defaultOptions, - ...options, - }); - }; + return useCallback( + (params: SearchParamValues, options?: UpdateSearchParamsOptions) => { + const nextSearchParams = new URLSearchParams(searchParamsRef.current?.toString() ?? ''); + + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === null) { + nextSearchParams.delete(key); + } else { + nextSearchParams.set(key, String(value)); + } + }); + + setSearchParams(nextSearchParams, { + ...defaultOptionsRef.current, + ...options, + }); + }, + [setSearchParams], + ); }; diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 43d6366f7..b53fe0baa 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -16,6 +16,7 @@ export type PeriodSelectorValue = '' | 'all' | '7d' | '14d' | '30d'; export type FindDocumentsOptions = { userId: number; teamId?: number; + team?: Awaited>; templateId?: number; source?: DocumentSource | DocumentSource[]; status?: ExtendedDocumentStatus | ExtendedDocumentStatus[]; @@ -34,6 +35,7 @@ export type FindDocumentsOptions = { export const findDocuments = async ({ userId, teamId, + team: preloadedTeam, templateId, source, status, @@ -45,25 +47,19 @@ export const findDocuments = async ({ query = '', folderId, }: FindDocumentsOptions) => { - const user = await prisma.user.findFirstOrThrow({ - where: { - id: userId, - }, - select: { - id: true, - email: true, - name: true, - }, - }); - - let team = null; - - if (teamId !== undefined) { - team = await getTeamById({ - userId, - teamId, - }); - } + const [user, team] = await Promise.all([ + prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + select: { + id: true, + email: true, + name: true, + }, + }), + preloadedTeam ?? (teamId !== undefined ? getTeamById({ userId, teamId }) : null), + ]); const orderByColumn = orderBy?.column ?? 'createdAt'; const orderByDirection = orderBy?.direction ?? 'desc'; diff --git a/packages/trpc/server/document-router/find-documents-internal.ts b/packages/trpc/server/document-router/find-documents-internal.ts index 2dfb38254..5a15c4c01 100644 --- a/packages/trpc/server/document-router/find-documents-internal.ts +++ b/packages/trpc/server/document-router/find-documents-internal.ts @@ -37,8 +37,10 @@ export const findDocumentsInternalRoute = authenticatedProcedure folderId, }; + let team: Awaited> | undefined; + if (teamId) { - const team = await getTeamById({ userId: user.id, teamId }); + team = await getTeamById({ userId: user.id, teamId }); getStatOptions.team = { teamId: team.id, @@ -55,6 +57,7 @@ export const findDocumentsInternalRoute = authenticatedProcedure findDocuments({ userId: user.id, teamId, + team, query, templateId, page,