From 06852b5d9ed6cc87638084e3e66eedb3b946cb9e Mon Sep 17 00:00:00 2001
From: Tim Hostetler <6970899+thostetler@users.noreply.github.com>
Date: Thu, 28 May 2026 13:11:21 -0400
Subject: [PATCH 1/5] feat(export): add ExportSkeleton loading placeholder
---
.../components/ExportSkeleton.test.tsx | 15 ++++++++++++
.../components/ExportSkeleton.tsx | 23 +++++++++++++++++++
2 files changed, 38 insertions(+)
create mode 100644 src/components/CitationExporter/components/ExportSkeleton.test.tsx
create mode 100644 src/components/CitationExporter/components/ExportSkeleton.tsx
diff --git a/src/components/CitationExporter/components/ExportSkeleton.test.tsx b/src/components/CitationExporter/components/ExportSkeleton.test.tsx
new file mode 100644
index 000000000..73f8eb54e
--- /dev/null
+++ b/src/components/CitationExporter/components/ExportSkeleton.test.tsx
@@ -0,0 +1,15 @@
+import { render, screen } from '@/test-utils';
+import { describe, expect, test } from 'vitest';
+import { ExportSkeleton } from './ExportSkeleton';
+
+describe('ExportSkeleton', () => {
+ test('renders the export container chrome with a heading slot', () => {
+ render();
+ expect(screen.getByTestId('export-heading')).toBeInTheDocument();
+ });
+
+ test('renders skeleton placeholders', () => {
+ const { container } = render();
+ expect(container.querySelectorAll('.chakra-skeleton').length).toBeGreaterThan(0);
+ });
+});
diff --git a/src/components/CitationExporter/components/ExportSkeleton.tsx b/src/components/CitationExporter/components/ExportSkeleton.tsx
new file mode 100644
index 000000000..84c676ba8
--- /dev/null
+++ b/src/components/CitationExporter/components/ExportSkeleton.tsx
@@ -0,0 +1,23 @@
+import { Grid, GridItem, Skeleton, Stack } from '@chakra-ui/react';
+import { ReactElement } from 'react';
+import { ExportContainer } from './ExportContainer';
+
+export const ExportSkeleton = (): ReactElement => {
+ return (
+ } isLoading>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
From aa012870bd838954f3fef1607ee3911ae04ecfd5 Mon Sep 17 00:00:00 2001
From: Tim Hostetler <6970899+thostetler@users.noreply.github.com>
Date: Thu, 28 May 2026 13:13:02 -0400
Subject: [PATCH 2/5] perf(export): drop bibcode and export prefetches from SSR
---
src/pages/search/exportcitation/[format].tsx | 61 ++++----------------
1 file changed, 10 insertions(+), 51 deletions(-)
diff --git a/src/pages/search/exportcitation/[format].tsx b/src/pages/search/exportcitation/[format].tsx
index dff43d088..1843abb5f 100644
--- a/src/pages/search/exportcitation/[format].tsx
+++ b/src/pages/search/exportcitation/[format].tsx
@@ -1,7 +1,6 @@
import { Alert, AlertIcon, Box, Flex, Heading, HStack } from '@chakra-ui/react';
import { ChevronLeftIcon } from '@chakra-ui/icons';
-import { getExportCitationDefaultContext } from '@/components/CitationExporter/CitationExporter.machine';
import { APP_DEFAULTS, BRAND_NAME_FULL } from '@/config';
import { useIsClient } from '@/lib/useIsClient';
import axios from 'axios';
@@ -21,8 +20,8 @@ import { unwrapStringValue } from '@/utils/common/formatters';
import { parseAPIError } from '@/utils/common/parseAPIError';
import { ExportApiFormatKey } from '@/api/export/types';
import { IADSApiSearchParams } from '@/api/search/types';
-import { fetchSearchInfinite, searchKeys, useSearchInfinite } from '@/api/search/search';
-import { exportCitationKeys, fetchExportCitation, fetchExportFormats } from '@/api/export/export';
+import { useSearchInfinite } from '@/api/search/search';
+import { exportCitationKeys, fetchExportFormats } from '@/api/export/export';
interface IExportCitationPageProps {
format: string;
@@ -131,83 +130,43 @@ const ExportCitationPage: NextPage = (props) => {
export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx) => {
const {
qid = null,
- p,
referrer = null,
...query
} = parseQueryFromUrl<{ qid: string; format: string }>(ctx.req.url, { sortPostfix: 'id asc' });
const { format } = ctx.params as { format: string };
- if (!query && !qid) {
- return {
- props: {
- format,
- query,
- qid,
- referrer,
- error: 'No Records',
- },
- };
- }
-
- const queryClient = new QueryClient();
- const params: IADSApiSearchParams = {
+ const searchParams: IADSApiSearchParams = {
rows: APP_DEFAULTS.EXPORT_PAGE_SIZE,
fl: ['bibcode'],
sort: query.sort ?? APP_DEFAULTS.SORT,
...(qid ? { q: `docs(${qid})` } : query),
};
- try {
- // primary search, this is based on query params
- const data = await queryClient.fetchInfiniteQuery({
- queryKey: searchKeys.infinite(params),
- queryFn: fetchSearchInfinite,
- meta: { params },
- });
+ const queryClient = new QueryClient();
+ try {
const formatsData = await queryClient.fetchQuery({
queryKey: exportCitationKeys.manifest(),
queryFn: fetchExportFormats,
});
const formats = map(prop('route'), formatsData).map((r) => r.substring(1));
-
- // extract bibcodes to use for export
- const records = data.pages[0].response.docs.map((d) => d.bibcode);
-
- const { params: exportParams } = getExportCitationDefaultContext({
- format: formats.includes(format) ? format : ExportApiFormatKey.bibtex,
- records,
- singleMode: false,
- sort: params.sort,
- });
-
- // fetch export string, format is pulled from the url
- void (await queryClient.prefetchQuery({
- queryKey: exportCitationKeys.primary(exportParams),
- queryFn: fetchExportCitation,
- meta: { params: exportParams },
- }));
-
- // react-query infinite queries cannot be serialized by next, currently.
- // see https://github.com/tannerlinsley/react-query/issues/3301#issuecomment-1041374043
-
- const dehydratedState = JSON.parse(JSON.stringify(dehydrate(queryClient)));
+ const resolvedFormat = formats.includes(format) ? format : ExportApiFormatKey.bibtex;
return {
props: {
- format: exportParams.format,
- query: params,
+ format: resolvedFormat,
+ query: searchParams,
referrer,
- dehydratedState,
+ dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
},
};
} catch (error) {
logger.error({ msg: 'GSSP error in export citation page', error });
return {
props: {
- query: params,
+ query: searchParams,
pageError: parseAPIError(error),
error: axios.isAxiosError(error) ? error.message : 'Unable to fetch data',
},
From 2c922f54e825a54a08f50b0e88e0cfe95dd7ca13 Mon Sep 17 00:00:00 2001
From: Tim Hostetler <6970899+thostetler@users.noreply.github.com>
Date: Thu, 28 May 2026 13:14:02 -0400
Subject: [PATCH 3/5] refactor(export): replace isClient toggle with loading
skeleton
---
src/pages/search/exportcitation/[format].tsx | 38 ++++++++------------
1 file changed, 14 insertions(+), 24 deletions(-)
diff --git a/src/pages/search/exportcitation/[format].tsx b/src/pages/search/exportcitation/[format].tsx
index 1843abb5f..020c33e00 100644
--- a/src/pages/search/exportcitation/[format].tsx
+++ b/src/pages/search/exportcitation/[format].tsx
@@ -2,7 +2,6 @@ import { Alert, AlertIcon, Box, Flex, Heading, HStack } from '@chakra-ui/react';
import { ChevronLeftIcon } from '@chakra-ui/icons';
import { APP_DEFAULTS, BRAND_NAME_FULL } from '@/config';
-import { useIsClient } from '@/lib/useIsClient';
import axios from 'axios';
import { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
@@ -14,6 +13,7 @@ import { useSettings } from '@/lib/useSettings';
import { logger } from '@/logger';
import { SimpleLink } from '@/components/SimpleLink';
import { CitationExporter } from '@/components/CitationExporter';
+import { ExportSkeleton } from '@/components/CitationExporter/components/ExportSkeleton';
import { JournalFormatMap } from '@/components/Settings';
import { parseQueryFromUrl } from '@/utils/common/search';
import { unwrapStringValue } from '@/utils/common/formatters';
@@ -34,10 +34,9 @@ interface IExportCitationPageProps {
}
const ExportCitationPage: NextPage = (props) => {
- const { format, query, referrer } = props;
- const isClient = useIsClient();
+ const { format, query, referrer, error } = props;
+ const router = useRouter();
- // get export related user settings
const { settings } = useSettings({
suspense: false,
});
@@ -57,22 +56,18 @@ const ExportCitationPage: NextPage = (props) => {
maxauthor: parseInt(settings.bibtexMaxAuthors),
};
- const router = useRouter();
- const { data, fetchNextPage, hasNextPage, error } = useSearchInfinite(query);
-
- // TODO: add more error handling here
- if (!data) {
- return null;
- }
+ const { data, fetchNextPage, hasNextPage, isLoading, error: searchError } = useSearchInfinite(query);
- const res = last(data?.pages).response;
- const records = res.docs.map((d) => d.bibcode);
- const numFound = res.numFound;
+ const lastPage = data ? last(data.pages) : null;
+ const records = lastPage ? lastPage.response.docs.map((d) => d.bibcode) : [];
+ const numFound = lastPage ? lastPage.response.numFound : 0;
const handleNextPage = () => {
void fetchNextPage();
};
+ const errorMessage = error?.message ?? (searchError instanceof Error ? searchError.message : undefined);
+
return (
<>
@@ -95,12 +90,14 @@ const ExportCitationPage: NextPage = (props) => {
- {error ? (
+ {errorMessage ? (
- {error.message}
+ {errorMessage}
- ) : isClient ? (
+ ) : isLoading || !data ? (
+
+ ) : (
= (props) => {
page={data.pages.length - 1}
sort={query.sort}
/>
- ) : (
-
)}
From 14cc973f72a28cb2c163bfefb8dbb3b0a193fd12 Mon Sep 17 00:00:00 2001
From: Tim Hostetler <6970899+thostetler@users.noreply.github.com>
Date: Thu, 28 May 2026 13:15:02 -0400
Subject: [PATCH 4/5] refactor(export): remove dead CitationExporter.Static
---
.../CitationExporter/CitationExporter.tsx | 42 -------------------
1 file changed, 42 deletions(-)
diff --git a/src/components/CitationExporter/CitationExporter.tsx b/src/components/CitationExporter/CitationExporter.tsx
index 04c0215c6..5af2b840b 100644
--- a/src/components/CitationExporter/CitationExporter.tsx
+++ b/src/components/CitationExporter/CitationExporter.tsx
@@ -279,45 +279,3 @@ const AdvancedControls = ({
}
return null;
};
-
-/**
- * Static component for SSR
- */
-const Static = (props: Omit): ReactElement => {
- const { records, initialFormat, singleMode, totalRecords, sort, ...divProps } = props;
-
- const { data, state } = useCitationExporter({
- format: initialFormat,
- records,
- singleMode: true,
- sort,
- });
- const ctx = state.context;
-
- const { getFormatById } = useExportFormats();
- const format = getFormatById(ctx.params.format);
-
- if (singleMode) {
- return (
- Exporting record in {format.name} format>} {...divProps}>
-
-
- );
- }
-
- return (
-
- Exporting record{ctx.range[1] - ctx.range[0] > 1 ? 's' : ''} {ctx.range[0] + 1} to {ctx.range[1]} (total:{' '}
- {totalRecords.toLocaleString()})
- >
- }
- {...divProps}
- >
-
-
- );
-};
-
-CitationExporter.Static = Static;
From 4d55fb7c5249aa7e1a87aa2235a011f3cd7cadd6 Mon Sep 17 00:00:00 2001
From: Tim Hostetler <6970899+thostetler@users.noreply.github.com>
Date: Thu, 28 May 2026 14:18:22 -0400
Subject: [PATCH 5/5] refactor(export): drop infinite-query serialization
workaround
---
src/pages/search/exportcitation/[format].tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pages/search/exportcitation/[format].tsx b/src/pages/search/exportcitation/[format].tsx
index 020c33e00..d66dc925c 100644
--- a/src/pages/search/exportcitation/[format].tsx
+++ b/src/pages/search/exportcitation/[format].tsx
@@ -149,7 +149,7 @@ export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx
format: resolvedFormat,
query: searchParams,
referrer,
- dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
+ dehydratedState: dehydrate(queryClient),
},
};
} catch (error) {