From 55d6e0a9f2989d502b2abe3e458aee097cb7383b Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Wed, 12 Nov 2025 15:42:01 -0500 Subject: [PATCH 1/2] remove prefetch helper, co-locate queryclient creation --- frontends/api/src/ssr/prefetch.ts | 31 ----- frontends/api/src/ssr/serverQueryClient.ts | 76 ------------ .../CoursewareDisplay/EnrollmentDisplay.tsx | 4 +- .../(products)/courses/[readable_id]/page.tsx | 20 +-- .../programs/[readable_id]/page.tsx | 20 +-- .../src/app/c/[channelType]/[name]/page.tsx | 34 +++--- .../[certificateType]/[uuid]/page.tsx | 33 ++--- .../[certificateType]/[uuid]/pdf/route.tsx | 4 +- frontends/main/src/app/departments/page.tsx | 14 ++- frontends/main/src/app/getQueryClient.ts | 96 +++++++++++++-- frontends/main/src/app/page.tsx | 114 ++++++++++-------- frontends/main/src/app/search/page.tsx | 16 ++- .../main/src/app/sitemaps/channels/sitemap.ts | 6 +- .../src/app/sitemaps/resources/sitemap.ts | 6 +- frontends/main/src/app/topics/page.tsx | 14 ++- frontends/main/src/app/units/page.tsx | 14 ++- frontends/main/src/common/metadata.ts | 4 +- 17 files changed, 257 insertions(+), 249 deletions(-) delete mode 100644 frontends/api/src/ssr/prefetch.ts delete mode 100644 frontends/api/src/ssr/serverQueryClient.ts diff --git a/frontends/api/src/ssr/prefetch.ts b/frontends/api/src/ssr/prefetch.ts deleted file mode 100644 index 3cd719911c..0000000000 --- a/frontends/api/src/ssr/prefetch.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { dehydrate } from "@tanstack/react-query" -import { getServerQueryClient } from "./serverQueryClient" -import type { Query } from "@tanstack/react-query" - -/** - * Utility to avoid repetition in server components - * Optionally pass the queryClient returned from a previous prefetch - * where queries are dependent on previous results - */ -export const prefetch = async ( - queries: (Query | unknown)[], - - /** - * Unless passed, the SSR QueryClient uses React's cache() for reuse for the duration of the request. - * - * The QueryClient is garbage collected once the dehydrated state is produced and - * sent to the client and the request is complete. - */ - queryClient = getServerQueryClient(), -) => { - await Promise.all( - queries.filter(Boolean).map((query) => { - return queryClient.prefetchQuery(query as Query) - }), - ) - - return { - dehydratedState: dehydrate(queryClient), - queryClient, - } -} diff --git a/frontends/api/src/ssr/serverQueryClient.ts b/frontends/api/src/ssr/serverQueryClient.ts deleted file mode 100644 index daa745891f..0000000000 --- a/frontends/api/src/ssr/serverQueryClient.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { cache } from "react" -import { QueryClient } from "@tanstack/react-query" -import { AxiosError } from "axios" - -const MAX_RETRIES = 3 -const NO_RETRY_CODES = [400, 401, 403, 404, 405, 409, 422] - -/** - * Get or create a server-side QueryClient for consistent retry behavior. - * The server QueryClient should be used for all server-side API calls. - * - * Uses React's cache() to ensure the same QueryClient instance is reused - * throughout a single HTTP request, enabling: - * - * - Server API calls share the same QueryClient: - * - Prefetch runs in page server components - * - generateMetadata() - * - No duplicate API calls within the same request - * - Automatic cleanup when the request completes - * - Isolation between different HTTP requests - * - * The QueryClientProvider runs (during SSR) in a separate render pass as it's a - * client component and so the instance is not reused. On the server this does not - * make API calls and only sets up the hydration boundary and registers hooks in - * readiness for the dehydrated state to be sent to the client. - */ -const getServerQueryClient = cache(() => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - /** - * React Query's default retry logic is only active in the browser. - * Here we explicitly configure it to retry MAX_RETRIES times on - * the server, with an exclusion list of statuses that we expect not - * to succeed on retry. - * - * Includes status undefined as we want to retry on network errors - */ - retry: (failureCount, error) => { - const axiosError = error as AxiosError - console.info("Retrying failed request", { - failureCount, - error: { - message: axiosError.message, - name: axiosError.name, - status: axiosError?.status, - code: axiosError.code, - method: axiosError.request?.method, - url: axiosError.request?.url, - }, - }) - const status = (error as AxiosError)?.response?.status - const isNetworkError = status === undefined || status === 0 - - if (isNetworkError || !NO_RETRY_CODES.includes(status)) { - return failureCount < MAX_RETRIES - } - return false - }, - - /** - * By default, React Query gradually applies a backoff delay, though it is - * preferable that we do not significantly delay initial page renders (or - * indeed pages that are Statically Rendered during the build process) and - * instead allow the request to fail quickly so it can be subsequently - * fetched on the client. - */ - retryDelay: 1000, - }, - }, - }) - - return queryClient -}) - -export { getServerQueryClient } diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx index 1f49b83826..5b59615eab 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx @@ -14,7 +14,7 @@ import { useQuery } from "@tanstack/react-query" import { userEnrollmentsToDashboardCourses } from "./transform" import { DashboardCard } from "./DashboardCard" import { DashboardCourse, EnrollmentStatus } from "./types" -import { MaybeHasStatusAndDetail } from "@/app/getQueryClient" +import type { AxiosError } from "axios" const Wrapper = styled.div(({ theme }) => ({ marginTop: "32px", @@ -183,7 +183,7 @@ const EnrollmentDisplay = () => { ...enrollmentQueries.courseRunEnrollmentsList(), select: userEnrollmentsToDashboardCourses, throwOnError: (error) => { - const err = error as MaybeHasStatusAndDetail + const err = error as AxiosError<{ detail?: string }> const status = err?.response?.status if ( status === 403 && diff --git a/frontends/main/src/app/(products)/courses/[readable_id]/page.tsx b/frontends/main/src/app/(products)/courses/[readable_id]/page.tsx index 96e47759ca..9d30c81e8f 100644 --- a/frontends/main/src/app/(products)/courses/[readable_id]/page.tsx +++ b/frontends/main/src/app/(products)/courses/[readable_id]/page.tsx @@ -1,13 +1,12 @@ import React from "react" -import { HydrationBoundary } from "@tanstack/react-query" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { safeGenerateMetadata, standardizeMetadata } from "@/common/metadata" -import { prefetch } from "api/ssr/prefetch" import CoursePage from "@/app-pages/ProductPages/CoursePage" import { notFound } from "next/navigation" import { pagesQueries } from "api/mitxonline-hooks/pages" import { coursesQueries } from "api/mitxonline-hooks/courses" import { DEFAULT_RESOURCE_IMG } from "ol-utilities" -import { getServerQueryClient } from "api/ssr/serverQueryClient" +import { getQueryClient } from "@/app/getQueryClient" export const generateMetadata = async ( props: PageProps<"/courses/[readable_id]">, @@ -15,7 +14,7 @@ export const generateMetadata = async ( const params = await props.params return safeGenerateMetadata(async () => { - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const data = await queryClient.fetchQuery( pagesQueries.coursePages(decodeURIComponent(params.readable_id)), @@ -44,12 +43,17 @@ const Page: React.FC> = async (props) => { * fetching via client, and calling notFound() if data missing. * This approach blocked by wagtail api requiring auth. */ - const { dehydratedState } = await prefetch([ - pagesQueries.coursePages(readableId), - coursesQueries.coursesList({ readable_id: readableId }), + const queryClient = getQueryClient() + + await Promise.all([ + queryClient.prefetchQuery(pagesQueries.coursePages(readableId)), + queryClient.prefetchQuery( + coursesQueries.coursesList({ readable_id: readableId }), + ), ]) + return ( - + ) diff --git a/frontends/main/src/app/(products)/programs/[readable_id]/page.tsx b/frontends/main/src/app/(products)/programs/[readable_id]/page.tsx index 094267e460..c6cc47cf28 100644 --- a/frontends/main/src/app/(products)/programs/[readable_id]/page.tsx +++ b/frontends/main/src/app/(products)/programs/[readable_id]/page.tsx @@ -1,13 +1,12 @@ import React from "react" -import { HydrationBoundary } from "@tanstack/react-query" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { safeGenerateMetadata, standardizeMetadata } from "@/common/metadata" -import { prefetch } from "api/ssr/prefetch" import ProgramPage from "@/app-pages/ProductPages/ProgramPage" import { notFound } from "next/navigation" import { pagesQueries } from "api/mitxonline-hooks/pages" import { programsQueries } from "api/mitxonline-hooks/programs" import { DEFAULT_RESOURCE_IMG } from "ol-utilities" -import { getServerQueryClient } from "api/ssr/serverQueryClient" +import { getQueryClient } from "@/app/getQueryClient" export const generateMetadata = async ( props: PageProps<"/programs/[readable_id]">, @@ -15,7 +14,7 @@ export const generateMetadata = async ( const params = await props.params return safeGenerateMetadata(async () => { - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const data = await queryClient.fetchQuery( pagesQueries.programPages(decodeURIComponent(params.readable_id)), @@ -46,12 +45,17 @@ const Page: React.FC> = async (props) => { * fetching via client, and calling notFound() if data missing. * This approach blocked by wagtail api requiring auth. */ - const { dehydratedState } = await prefetch([ - pagesQueries.programPages(readableId), - programsQueries.programsList({ readable_id: readableId }), + const queryClient = getQueryClient() + + await Promise.all([ + queryClient.prefetchQuery(pagesQueries.programPages(readableId)), + queryClient.prefetchQuery( + programsQueries.programsList({ readable_id: readableId }), + ), ]) + return ( - + ) diff --git a/frontends/main/src/app/c/[channelType]/[name]/page.tsx b/frontends/main/src/app/c/[channelType]/[name]/page.tsx index 236153b240..bbe9af0cba 100644 --- a/frontends/main/src/app/c/[channelType]/[name]/page.tsx +++ b/frontends/main/src/app/c/[channelType]/[name]/page.tsx @@ -1,6 +1,5 @@ import React from "react" import ChannelPage from "@/app-pages/ChannelPage/ChannelPage" -import { getServerQueryClient } from "api/ssr/serverQueryClient" import { ChannelTypeEnum, UnitChannel } from "api/v0" import { FeaturedListOfferedByEnum, @@ -9,8 +8,7 @@ import { LearningResourceOfferorDetail, } from "api" import { getMetadataAsync, safeGenerateMetadata } from "@/common/metadata" -import { HydrationBoundary } from "@tanstack/react-query" -import { prefetch } from "api/ssr/prefetch" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { learningResourceQueries, offerorQueries, @@ -25,6 +23,7 @@ import { } from "@/app-pages/ChannelPage/searchRequests" import { isInEnum } from "@/common/utils" import { notFound } from "next/navigation" +import { getQueryClient } from "@/app/getQueryClient" export async function generateMetadata({ searchParams, @@ -33,7 +32,7 @@ export async function generateMetadata({ const { channelType, name } = await params return safeGenerateMetadata(async () => { - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const data = await queryClient.fetchQuery( channelQueries.detailByType(channelType, name), @@ -58,16 +57,20 @@ const Page: React.FC> = async ({ const search = await searchParams - const { queryClient } = await prefetch([ - offerorQueries.list({}), + const queryClient = getQueryClient() + + await Promise.all([ + queryClient.prefetchQuery(offerorQueries.list({})), channelType === ChannelTypeEnum.Unit && - learningResourceQueries.featured({ - limit: 12, - offered_by: [name as FeaturedListOfferedByEnum], - }), + queryClient.prefetchQuery( + learningResourceQueries.featured({ + limit: 12, + offered_by: [name as FeaturedListOfferedByEnum], + }), + ), channelType === ChannelTypeEnum.Unit && - testimonialsQueries.list({ offerors: [name] }), - channelQueries.detailByType(channelType, name), + queryClient.prefetchQuery(testimonialsQueries.list({ offerors: [name] })), + queryClient.prefetchQuery(channelQueries.detailByType(channelType, name)), ]) const channel = queryClient.getQueryData( @@ -102,13 +105,12 @@ const Page: React.FC> = async ({ page: Number(search.page ?? 1), }) - const { dehydratedState } = await prefetch( - [learningResourceQueries.search(searchRequest as LRSearchRequest)], - queryClient, + await queryClient.prefetchQuery( + learningResourceQueries.search(searchRequest as LRSearchRequest), ) return ( - + ) diff --git a/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx b/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx index 60aca8da2f..996bdeecfe 100644 --- a/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx +++ b/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx @@ -1,13 +1,12 @@ import React from "react" import { Metadata } from "next" import CertificatePage from "@/app-pages/CertificatePage/CertificatePage" -import { prefetch } from "api/ssr/prefetch" import { certificateQueries } from "api/mitxonline-hooks/certificates" -import { HydrationBoundary } from "@tanstack/react-query" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { isInEnum } from "@/common/utils" import { notFound } from "next/navigation" import { safeGenerateMetadata, standardizeMetadata } from "@/common/metadata" -import { getServerQueryClient } from "api/ssr/serverQueryClient" +import { getQueryClient } from "@/app/getQueryClient" const { NEXT_PUBLIC_ORIGIN } = process.env @@ -24,7 +23,7 @@ export async function generateMetadata({ return safeGenerateMetadata(async () => { let title, displayType, userName - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() if (certificateType === CertificateType.Course) { const data = await queryClient.fetchQuery( @@ -68,18 +67,24 @@ const Page: React.FC< notFound() } - const { dehydratedState } = await prefetch([ - certificateType === CertificateType.Course - ? certificateQueries.courseCertificatesRetrieve({ - cert_uuid: uuid, - }) - : certificateQueries.programCertificatesRetrieve({ - cert_uuid: uuid, - }), - ]) + const queryClient = getQueryClient() + + if (certificateType === CertificateType.Course) { + await queryClient.prefetchQuery( + certificateQueries.courseCertificatesRetrieve({ + cert_uuid: uuid, + }), + ) + } else { + await queryClient.prefetchQuery( + certificateQueries.programCertificatesRetrieve({ + cert_uuid: uuid, + }), + ) + } return ( - + > = async () => { - const { dehydratedState } = await prefetch([ - channelQueries.countsByType("department"), - schoolQueries.list(), + const queryClient = getQueryClient() + + await Promise.all([ + queryClient.prefetchQuery(channelQueries.countsByType("department")), + queryClient.prefetchQuery(schoolQueries.list()), ]) return ( - + ) diff --git a/frontends/main/src/app/getQueryClient.ts b/frontends/main/src/app/getQueryClient.ts index 4c2ce1f6ce..38f2fb0a7e 100644 --- a/frontends/main/src/app/getQueryClient.ts +++ b/frontends/main/src/app/getQueryClient.ts @@ -1,21 +1,92 @@ // Based on https://tanstack.com/query/v5/docs/framework/react/guides/advanced-ssr import { QueryClient, isServer, focusManager } from "@tanstack/react-query" -import { getServerQueryClient } from "api/ssr/serverQueryClient" - -type MaybeHasStatusAndDetail = { - response?: { - status?: number - data?: { - detail?: string - } - } -} +import type { AxiosError } from "axios" +import { cache } from "react" const MAX_RETRIES = 3 const THROW_ERROR_CODES = [400, 401, 403] const NO_RETRY_CODES = [400, 401, 403, 404, 405, 409, 422] +/** + * Get or create a server-side QueryClient for consistent retry behavior. + * The server QueryClient should be used for all server-side API calls. + * + * Uses React's cache() to ensure the same QueryClient instance is reused + * throughout a single HTTP request, enabling: + * + * - Server API calls share the same QueryClient: + * - Prefetch runs in page server components + * - generateMetadata() + * - No duplicate API calls within the same request + * - Automatic cleanup when the request completes + * - Isolation between different HTTP requests + * + * The QueryClientProvider runs (during SSR) in a separate render pass as it's a + * client component and so the instance is not reused. On the server this does not + * make API calls and only sets up the hydration boundary and registers hooks in + * readiness for the dehydrated state to be sent to the client. + */ +const getServerQueryClient = cache(() => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + /** + * We create a new query client per request, but still need a staletime + * to avoid marking queries as stale during the server-side render pass. + * That can cause hydration errors if client renders differently when + * the requests are stale (e.g., dependence on isFetching) + * + * The exact value here isn't important, as long as it's bigger than + * request duration. + */ + staleTime: 15 * 60 * 1000, + + /** + * React Query's default retry logic is only active in the browser. + * Here we explicitly configure it to retry MAX_RETRIES times on + * the server, with an exclusion list of statuses that we expect not + * to succeed on retry. + * + * Includes status undefined as we want to retry on network errors + */ + retry: (failureCount, error) => { + const axiosError = error as AxiosError + console.info("Retrying failed request", { + failureCount, + error: { + message: axiosError.message, + name: axiosError.name, + status: axiosError?.status, + code: axiosError.code, + method: axiosError.request?.method, + url: axiosError.request?.url, + }, + }) + const status = (error as AxiosError)?.response?.status + const isNetworkError = status === undefined || status === 0 + + if (isNetworkError || !NO_RETRY_CODES.includes(status)) { + return failureCount < MAX_RETRIES + } + return false + }, + + /** + * By default, React Query gradually applies a backoff delay, though it is + * preferable that we do not significantly delay initial page renders (or + * indeed pages that are Statically Rendered during the build process) and + * instead allow the request to fail quickly so it can be subsequently + * fetched on the client. + */ + retryDelay: 1000, + }, + }, + }) + + return queryClient +}) + const makeBrowserQueryClient = (): QueryClient => { return new QueryClient({ defaultOptions: { @@ -33,12 +104,12 @@ const makeBrowserQueryClient = (): QueryClient => { * be handled locally by components. */ throwOnError: (error) => { - const status = (error as MaybeHasStatusAndDetail)?.response?.status + const status = (error as AxiosError)?.response?.status return THROW_ERROR_CODES.includes(status ?? 0) }, retry: (failureCount, error) => { - const status = (error as MaybeHasStatusAndDetail)?.response?.status + const status = (error as AxiosError)?.response?.status const isNetworkError = status === undefined || status === 0 /** @@ -128,4 +199,3 @@ focusManager.setEventListener((setFocused) => { }) export { makeBrowserQueryClient, getQueryClient } -export type { MaybeHasStatusAndDetail } diff --git a/frontends/main/src/app/page.tsx b/frontends/main/src/app/page.tsx index 027d19e74a..f9aae6bc27 100644 --- a/frontends/main/src/app/page.tsx +++ b/frontends/main/src/app/page.tsx @@ -2,7 +2,7 @@ import React from "react" import type { Metadata } from "next" import HomePage from "@/app-pages/HomePage/HomePage" import { getMetadataAsync, safeGenerateMetadata } from "@/common/metadata" -import { HydrationBoundary } from "@tanstack/react-query" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { learningResourceQueries, topicQueries, @@ -12,7 +12,7 @@ import { NewsEventsListFeedTypeEnum, newsEventsQueries, } from "api/hooks/newsEvents" -import { prefetch } from "api/ssr/prefetch" +import { getQueryClient } from "@/app/getQueryClient" export async function generateMetadata({ searchParams, @@ -26,63 +26,83 @@ export async function generateMetadata({ } const Page: React.FC> = async () => { - const { dehydratedState } = await prefetch([ + const queryClient = getQueryClient() + + await Promise.all([ // Featured Courses carousel "All" - learningResourceQueries.featured({ - limit: 12, - }), + queryClient.prefetchQuery( + learningResourceQueries.featured({ + limit: 12, + }), + ), // Featured Courses carousel "Free" - learningResourceQueries.featured({ - limit: 12, - free: true, - }), + queryClient.prefetchQuery( + learningResourceQueries.featured({ + limit: 12, + free: true, + }), + ), // Featured Courses carousel "With Certificate" - learningResourceQueries.featured({ - limit: 12, - certification: true, - professional: false, - }), + queryClient.prefetchQuery( + learningResourceQueries.featured({ + limit: 12, + certification: true, + professional: false, + }), + ), // Featured Courses carousel "Professional & Executive Learning" - learningResourceQueries.featured({ - limit: 12, - professional: true, - }), + queryClient.prefetchQuery( + learningResourceQueries.featured({ + limit: 12, + professional: true, + }), + ), // Media carousel "All" - learningResourceQueries.list({ - resource_type: ["video", "podcast_episode"], - limit: 12, - sortby: "new", - }), + queryClient.prefetchQuery( + learningResourceQueries.list({ + resource_type: ["video", "podcast_episode"], + limit: 12, + sortby: "new", + }), + ), // Media carousel "Videos" - learningResourceQueries.list({ - resource_type: ["video"], - limit: 12, - sortby: "new", - }), + queryClient.prefetchQuery( + learningResourceQueries.list({ + resource_type: ["video"], + limit: 12, + sortby: "new", + }), + ), // Media carousel "Podcasts" - learningResourceQueries.list({ - resource_type: ["podcast_episode"], - limit: 12, - sortby: "new", - }), + queryClient.prefetchQuery( + learningResourceQueries.list({ + resource_type: ["podcast_episode"], + limit: 12, + sortby: "new", + }), + ), // Browse by Topic - topicQueries.list({ is_toplevel: true }), + queryClient.prefetchQuery(topicQueries.list({ is_toplevel: true })), - testimonialsQueries.list({ position: 1 }), - newsEventsQueries.list({ - feed_type: [NewsEventsListFeedTypeEnum.Events], - limit: 5, - sortby: "event_date", - }), - newsEventsQueries.list({ - feed_type: [NewsEventsListFeedTypeEnum.News], - limit: 6, - sortby: "-news_date", - }), + queryClient.prefetchQuery(testimonialsQueries.list({ position: 1 })), + queryClient.prefetchQuery( + newsEventsQueries.list({ + feed_type: [NewsEventsListFeedTypeEnum.Events], + limit: 5, + sortby: "event_date", + }), + ), + queryClient.prefetchQuery( + newsEventsQueries.list({ + feed_type: [NewsEventsListFeedTypeEnum.News], + limit: 6, + sortby: "-news_date", + }), + ), ]) return ( - + ) diff --git a/frontends/main/src/app/search/page.tsx b/frontends/main/src/app/search/page.tsx index 234f92e618..22cfc70244 100644 --- a/frontends/main/src/app/search/page.tsx +++ b/frontends/main/src/app/search/page.tsx @@ -1,6 +1,5 @@ import React from "react" -import { HydrationBoundary } from "@tanstack/react-query" -import { prefetch } from "api/ssr/prefetch" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { learningResourceQueries, offerorQueries, @@ -12,6 +11,7 @@ import getSearchParams from "@/page-components/SearchDisplay/getSearchParams" import validateRequestParams from "@/page-components/SearchDisplay/validateRequestParams" import type { ResourceSearchRequest } from "@/page-components/SearchDisplay/validateRequestParams" import { LearningResourcesSearchApiLearningResourcesSearchRetrieveRequest as LRSearchRequest } from "api" +import { getQueryClient } from "@/app/getQueryClient" export async function generateMetadata({ searchParams }: PageProps<"/search">) { return safeGenerateMetadata(async () => { @@ -34,13 +34,17 @@ const Page: React.FC> = async ({ searchParams }) => { page: Number(search.page ?? 1), }) - const { dehydratedState } = await prefetch([ - offerorQueries.list({}), - learningResourceQueries.search(params as LRSearchRequest), + const queryClient = getQueryClient() + + await Promise.all([ + queryClient.prefetchQuery(offerorQueries.list({})), + queryClient.prefetchQuery( + learningResourceQueries.search(params as LRSearchRequest), + ), ]) return ( - + ) diff --git a/frontends/main/src/app/sitemaps/channels/sitemap.ts b/frontends/main/src/app/sitemaps/channels/sitemap.ts index 5cf08fe599..76e9b5b6ac 100644 --- a/frontends/main/src/app/sitemaps/channels/sitemap.ts +++ b/frontends/main/src/app/sitemaps/channels/sitemap.ts @@ -2,7 +2,7 @@ import type { MetadataRoute } from "next" import invariant from "tiny-invariant" import type { GenerateSitemapResult } from "../types" import { dangerouslyDetectProductionBuildPhase } from "../util" -import { getServerQueryClient } from "api/ssr/serverQueryClient" +import { getQueryClient } from "@/app/getQueryClient" import { channelQueries } from "api/hooks/channels" const BASE_URL = process.env.NEXT_PUBLIC_ORIGIN @@ -23,7 +23,7 @@ export async function generateSitemaps(): Promise { * Early exit here to avoid the useless build-time API calls. */ if (dangerouslyDetectProductionBuildPhase()) return [] - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const { count } = await queryClient.fetchQuery( channelQueries.list({ limit: PAGE_SIZE }), ) @@ -40,7 +40,7 @@ export default async function sitemap({ }: { id: string }): Promise { - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const data = await queryClient.fetchQuery( channelQueries.list({ limit: PAGE_SIZE, diff --git a/frontends/main/src/app/sitemaps/resources/sitemap.ts b/frontends/main/src/app/sitemaps/resources/sitemap.ts index 80975d14ca..3c28f7ad3a 100644 --- a/frontends/main/src/app/sitemaps/resources/sitemap.ts +++ b/frontends/main/src/app/sitemaps/resources/sitemap.ts @@ -1,5 +1,5 @@ import type { MetadataRoute } from "next" -import { getServerQueryClient } from "api/ssr/serverQueryClient" +import { getQueryClient } from "@/app/getQueryClient" import { learningResourceQueries } from "api/hooks/learningResources" import invariant from "tiny-invariant" import { GenerateSitemapResult } from "../types" @@ -23,7 +23,7 @@ export async function generateSitemaps(): Promise { * Early exit here to avoid the useless build-time API calls. */ if (dangerouslyDetectProductionBuildPhase()) return [] - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const { count } = await queryClient.fetchQuery( learningResourceQueries.summaryList({ limit: PAGE_SIZE, @@ -44,7 +44,7 @@ export default async function sitemap({ }: { id: string }): Promise { - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const data = await queryClient.fetchQuery( learningResourceQueries.summaryList({ limit: PAGE_SIZE, diff --git a/frontends/main/src/app/topics/page.tsx b/frontends/main/src/app/topics/page.tsx index b60ebdbf6b..ad6d56998b 100644 --- a/frontends/main/src/app/topics/page.tsx +++ b/frontends/main/src/app/topics/page.tsx @@ -1,10 +1,10 @@ import React from "react" import { Metadata } from "next" -import { HydrationBoundary } from "@tanstack/react-query" -import { prefetch } from "api/ssr/prefetch" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { topicQueries } from "api/hooks/learningResources" import { channelQueries } from "api/hooks/channels" import TopicsListingPage from "@/app-pages/TopicsListingPage/TopicsListingPage" +import { getQueryClient } from "@/app/getQueryClient" import { standardizeMetadata } from "@/common/metadata" export const metadata: Metadata = standardizeMetadata({ @@ -12,13 +12,15 @@ export const metadata: Metadata = standardizeMetadata({ }) const Page: React.FC> = async () => { - const { dehydratedState } = await prefetch([ - topicQueries.list({}), - channelQueries.countsByType("topic"), + const queryClient = getQueryClient() + + await Promise.all([ + queryClient.prefetchQuery(topicQueries.list({})), + queryClient.prefetchQuery(channelQueries.countsByType("topic")), ]) return ( - + ) diff --git a/frontends/main/src/app/units/page.tsx b/frontends/main/src/app/units/page.tsx index 820179a475..4770a5e329 100644 --- a/frontends/main/src/app/units/page.tsx +++ b/frontends/main/src/app/units/page.tsx @@ -1,23 +1,25 @@ import React from "react" import { Metadata } from "next" -import { HydrationBoundary } from "@tanstack/react-query" -import { prefetch } from "api/ssr/prefetch" +import { HydrationBoundary, dehydrate } from "@tanstack/react-query" import { standardizeMetadata } from "@/common/metadata" import { channelQueries } from "api/hooks/channels" import UnitsListingPage from "@/app-pages/UnitsListingPage/UnitsListingPage" +import { getQueryClient } from "@/app/getQueryClient" export const metadata: Metadata = standardizeMetadata({ title: "Units", }) const Page: React.FC> = async () => { - const { dehydratedState } = await prefetch([ - channelQueries.countsByType("unit"), - channelQueries.list({ channel_type: "unit" }), + const queryClient = getQueryClient() + + await Promise.all([ + queryClient.prefetchQuery(channelQueries.countsByType("unit")), + queryClient.prefetchQuery(channelQueries.list({ channel_type: "unit" })), ]) return ( - + ) diff --git a/frontends/main/src/common/metadata.ts b/frontends/main/src/common/metadata.ts index 77f331f777..1a7adce35d 100644 --- a/frontends/main/src/common/metadata.ts +++ b/frontends/main/src/common/metadata.ts @@ -6,8 +6,8 @@ import type { AxiosError } from "axios" import type { Metadata } from "next" import * as Sentry from "@sentry/nextjs" import { learningResourceQueries } from "api/hooks/learningResources" -import { getServerQueryClient } from "api/ssr/serverQueryClient" import { notFound } from "next/navigation" +import { getQueryClient } from "@/app/getQueryClient" const DEFAULT_OG_IMAGE = "/images/learn-og-image.jpg" @@ -71,7 +71,7 @@ export const getMetadataAsync = async ({ const alts = alternates ?? {} if (learningResourceId) { - const queryClient = getServerQueryClient() + const queryClient = getQueryClient() const data = await queryClient.fetchQuery( learningResourceQueries.detail(learningResourceId), ) From b70db3afb4d81acac9849975718db787b1959fd4 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Fri, 14 Nov 2025 13:29:46 -0500 Subject: [PATCH 2/2] fix typo --- frontends/main/src/app/getQueryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontends/main/src/app/getQueryClient.ts b/frontends/main/src/app/getQueryClient.ts index 38f2fb0a7e..1849cfd3a1 100644 --- a/frontends/main/src/app/getQueryClient.ts +++ b/frontends/main/src/app/getQueryClient.ts @@ -32,7 +32,7 @@ const getServerQueryClient = cache(() => { defaultOptions: { queries: { /** - * We create a new query client per request, but still need a staletime + * We create a new query client per request, but still need a staleTime * to avoid marking queries as stale during the server-side render pass. * That can cause hydration errors if client renders differently when * the requests are stale (e.g., dependence on isFetching)