Skip to content

Commit b9a9d45

Browse files
jonkaftonmbertrand
authored andcommitted
Server rendering for homepage, units and topics listing pages (#1847)
* Warn on API calls during initial render not prefetched * Full prefetch for homepage (commented) * Prefetch utility * Check for queries prefetched that are not needed during render and warn * No need to stringify * Replace useQuery overrides with decoupled cache check (wip) * Observer count for unnecessary prefetch warnings * Remove useQuery override * Test prefetch warnings * Remove inadvertent/unnecessary diff * Remove comments * Prefetch homepage. Invalidate learning resource cache items * Remove comment * Update comment * Temp remove featured resource list shuffle * Remove unused * Prefetch calls * Prefetch topics page * Rename key factory re-exports * Units page prefetch * Single request for all unit channel details * Update unit listing page tests * Page arg types * Optional route params * Remove comment * Remove unused * Remove comment * Reinstate featured list shuffle and remove cache invalidation to refetch resources for user * Add news to server render
1 parent 8d8d5f6 commit b9a9d45

File tree

27 files changed

+272
-173
lines changed

27 files changed

+272
-173
lines changed

frontends/api/src/hooks/channels/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ export {
5555
useChannelsList,
5656
useChannelPartialUpdate,
5757
useChannelCounts,
58-
channels as channelsKeyFactory,
58+
channels,
5959
}

frontends/api/src/hooks/learningResources/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,5 +150,5 @@ export {
150150
useSchoolsList,
151151
useSimilarLearningResources,
152152
useVectorSimilarLearningResources,
153-
learningResources as learningResourcesKeyFactory,
153+
learningResources,
154154
}

frontends/api/src/hooks/newsEvents/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ export {
1919
useNewsEventsList,
2020
useNewsEventsDetail,
2121
NewsEventsListFeedTypeEnum,
22-
newsEvents as newsEventsKeyFactory,
22+
newsEvents,
2323
}

frontends/api/src/hooks/testimonials/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,4 @@ const useTestimonialDetail = (id: number | undefined) => {
2323
})
2424
}
2525

26-
export {
27-
useTestimonialDetail,
28-
useTestimonialList,
29-
testimonials as testimonialsKeyFactory,
30-
}
26+
export { useTestimonialDetail, useTestimonialList, testimonials }

frontends/api/src/ssr/prefetch.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { QueryClient, dehydrate } from "@tanstack/react-query"
22
import type { Query } from "@tanstack/react-query"
33

4-
// Utility to avoid repetition in server components
5-
export const prefetch = async (queries: (Query | unknown)[]) => {
6-
const queryClient = new QueryClient()
4+
/* Utility to avoid repetition in server components
5+
* Optionally pass the queryClient returned from a previous prefetch
6+
* where queries are dependent on previous results
7+
*/
8+
export const prefetch = async (
9+
queries: (Query | unknown)[],
10+
queryClient?: QueryClient,
11+
) => {
12+
queryClient = queryClient || new QueryClient()
713

814
await Promise.all(
915
queries.map((query) => queryClient.prefetchQuery(query as Query)),
1016
)
1117

12-
return dehydrate(queryClient)
18+
return { dehydratedState: dehydrate(queryClient), queryClient }
1319
}

frontends/api/src/ssr/usePrefetchWarnings.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { usePrefetchWarnings } from "./usePrefetchWarnings"
44
import { setupReactQueryTest } from "../hooks/test-utils"
55
import { urls, factories, setMockResponse } from "../test-utils"
66
import {
7-
learningResourcesKeyFactory,
7+
learningResources,
88
useLearningResourcesDetail,
99
} from "../hooks/learningResources"
1010

@@ -45,7 +45,7 @@ describe("SSR prefetch warnings", () => {
4545
expect.objectContaining({
4646
disabled: false,
4747
initialStatus: "loading",
48-
key: learningResourcesKeyFactory.detail(1).queryKey,
48+
key: learningResources.detail(1).queryKey,
4949
observerCount: 1,
5050
}),
5151
],
@@ -65,7 +65,7 @@ describe("SSR prefetch warnings", () => {
6565
wrapper,
6666
initialProps: {
6767
queryClient,
68-
exemptions: [learningResourcesKeyFactory.detail(1).queryKey],
68+
exemptions: [learningResources.detail(1).queryKey],
6969
},
7070
})
7171

@@ -83,7 +83,7 @@ describe("SSR prefetch warnings", () => {
8383
const { unmount } = renderHook(
8484
() =>
8585
useQuery({
86-
...learningResourcesKeyFactory.detail(1),
86+
...learningResources.detail(1),
8787
initialData: data,
8888
}),
8989
{ wrapper },
@@ -105,9 +105,9 @@ describe("SSR prefetch warnings", () => {
105105
[
106106
{
107107
disabled: false,
108-
hash: JSON.stringify(learningResourcesKeyFactory.detail(1).queryKey),
108+
hash: JSON.stringify(learningResources.detail(1).queryKey),
109109
initialStatus: "success",
110-
key: learningResourcesKeyFactory.detail(1).queryKey,
110+
key: learningResources.detail(1).queryKey,
111111
observerCount: 0,
112112
status: "success",
113113
},

frontends/api/src/test-utils/factories/channels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ const _channelShared = (): Partial<Omit<Channel, "channel_type">> => {
148148
key: faker.lorem.slug(),
149149
value: faker.lorem.slug(),
150150
}),
151+
channel_url: `${faker.internet.url({ appendSlash: false })}${faker.system.directoryPath()}`,
151152
}
152153
}
153154

frontends/api/src/test-utils/urls.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import type {
99
NewsEventsApiNewsEventsListRequest,
1010
TestimonialsApi,
11+
ChannelsApi,
1112
} from "../generated/v0"
1213
import type {
1314
LearningResourcesApi as LRApi,
@@ -186,6 +187,8 @@ const channels = {
186187
details: (channelType: string, name: string) =>
187188
`${API_BASE_URL}/api/v0/channels/type/${channelType}/${name}/`,
188189
patch: (id: number) => `${API_BASE_URL}/api/v0/channels/${id}/`,
190+
list: (params?: Paramsv0<ChannelsApi, "channelsList">) =>
191+
`${API_BASE_URL}/api/v0/channels/${query(params)}`,
189192
}
190193

191194
const widgetLists = {

frontends/main/src/app-pages/UnitsListingPage/UnitCard.tsx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react"
2-
import { LearningResourceOfferorDetail, OfferedByEnum } from "api"
2+
import type { OfferedByEnum } from "api"
3+
import type { UnitChannel } from "api/v0"
34
import {
45
Card,
56
Skeleton,
@@ -8,7 +9,6 @@ import {
89
theme,
910
UnitLogo,
1011
} from "ol-components"
11-
import { useChannelDetail } from "api/hooks/channels"
1212
import Link from "next/link"
1313

1414
const CardStyled = styled(Card)({
@@ -102,25 +102,23 @@ const CountsText = styled(Typography)(({ theme }) => ({
102102
}))
103103

104104
interface UnitCardsProps {
105-
units: LearningResourceOfferorDetail[] | undefined
105+
channels: UnitChannel[] | undefined
106106
courseCounts: Record<string, number>
107107
programCounts: Record<string, number>
108108
}
109109

110110
interface UnitCardProps {
111-
unit: LearningResourceOfferorDetail
111+
channel: UnitChannel
112112
courseCount: number
113113
programCount: number
114114
}
115115

116116
const UnitCard: React.FC<UnitCardProps> = (props) => {
117-
const { unit, courseCount, programCount } = props
118-
const channelDetailQuery = useChannelDetail("unit", unit.code)
119-
const channelDetail = channelDetailQuery.data
120-
const unitUrl = channelDetail?.channel_url
117+
const { channel, courseCount, programCount } = props
118+
const unit = channel.unit_detail.unit
121119

122-
if (!unitUrl) return null
123-
const href = unitUrl && new URL(unitUrl).pathname
120+
if (!channel.channel_url) return null
121+
const href = new URL(channel.channel_url).pathname
124122

125123
return (
126124
<CardStyled forwardClicksToLink data-testid={`unit-card-${unit.code}`}>
@@ -134,9 +132,7 @@ const UnitCard: React.FC<UnitCardProps> = (props) => {
134132
</LogoContainer>
135133
<CardBottom>
136134
<ValuePropContainer>
137-
<HeadingText>
138-
{channelDetail?.configuration?.heading}
139-
</HeadingText>
135+
<HeadingText>{channel?.configuration?.heading}</HeadingText>
140136
</ValuePropContainer>
141137
<CountsTextContainer>
142138
<CountsText data-testid={`course-count-${unit.code}`}>
@@ -174,17 +170,18 @@ export const UnitCardLoading = () => {
174170
}
175171

176172
export const UnitCards: React.FC<UnitCardsProps> = (props) => {
177-
const { units, courseCounts, programCounts } = props
173+
const { channels, courseCounts, programCounts } = props
178174
return (
179175
<>
180-
{units?.map((unit) => {
176+
{channels?.map((channel) => {
177+
const unit = channel.unit_detail.unit
181178
const courseCount = courseCounts[unit.code] || 0
182179
const programCount = programCounts[unit.code] || 0
183180

184181
return unit.value_prop ? (
185182
<UnitCard
186183
key={unit.code}
187-
unit={unit}
184+
channel={channel}
188185
courseCount={courseCount}
189186
programCount={programCount}
190187
/>

frontends/main/src/app-pages/UnitsListingPage/UnitsListingPage.test.tsx

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,71 @@ import React from "react"
22
import { renderWithProviders, screen, waitFor, within } from "@/test-utils"
33
import UnitsListingPage from "./UnitsListingPage"
44
import { factories, setMockResponse, urls } from "api/test-utils"
5+
import { ChannelTypeEnum } from "api/v0"
6+
import type { UnitChannel } from "api/v0"
57
import { assertHeadings } from "ol-test-utilities"
68

7-
describe("DepartmentListingPage", () => {
9+
describe("UnitListingPage", () => {
810
const setupApis = () => {
9-
const make = factories.learningResources
10-
const academicUnit1 = make.offeror({
11-
code: "academicUnit1",
12-
name: "Academic Unit 1",
13-
value_prop: "Academic Unit 1 value prop",
14-
professional: false,
11+
const make = factories.channels
12+
const academicUnit1 = make.channel({
13+
channel_type: ChannelTypeEnum.Unit,
14+
name: "academicUnit1",
15+
title: "Academic Unit 1",
16+
unit_detail: {
17+
unit: {
18+
value_prop: "Academic Unit 1 value prop",
19+
professional: false,
20+
},
21+
},
1522
})
16-
const academicUnit2 = make.offeror({
17-
code: "academicUnit2",
18-
name: "Academic Unit 2",
19-
value_prop: "Academic Unit 2 value prop",
20-
professional: false,
23+
const academicUnit2 = make.channel({
24+
channel_type: ChannelTypeEnum.Unit,
25+
name: "academicUnit2",
26+
title: "Academic Unit 2",
27+
unit_detail: {
28+
unit: {
29+
value_prop: "Academic Unit 2 value prop",
30+
professional: false,
31+
},
32+
},
2133
})
22-
const academicUnit3 = make.offeror({
23-
code: "academicUnit3",
24-
name: "Academic Unit 3",
25-
value_prop: "Academic Unit 3 value prop",
26-
professional: false,
34+
const academicUnit3 = make.channel({
35+
channel_type: ChannelTypeEnum.Unit,
36+
name: "academicUnit3",
37+
title: "Academic Unit 3",
38+
unit_detail: {
39+
unit: {
40+
value_prop: "Academic Unit 3 value prop",
41+
professional: false,
42+
},
43+
},
2744
})
2845

29-
const professionalUnit1 = make.offeror({
30-
code: "professionalUnit1",
31-
name: "Professional Unit 1",
32-
value_prop: "Professional Unit 1 value prop",
33-
professional: true,
46+
const professionalUnit1 = make.channel({
47+
channel_type: ChannelTypeEnum.Unit,
48+
name: "professionalUnit1",
49+
title: "Professional Unit 1",
50+
unit_detail: {
51+
unit: {
52+
value_prop: "Professional Unit 1 value prop",
53+
professional: true,
54+
},
55+
},
3456
})
35-
const professionalUnit2 = make.offeror({
36-
code: "professionalUnit2",
37-
name: "Professional Unit 2",
38-
value_prop: "Professional Unit 2 value prop",
39-
professional: true,
57+
const professionalUnit2 = make.channel({
58+
channel_type: ChannelTypeEnum.Unit,
59+
name: "professionalUnit2",
60+
title: "Professional Unit 2",
61+
unit_detail: {
62+
unit: {
63+
value_prop: "Professional Unit 2 value prop",
64+
professional: true,
65+
},
66+
},
4067
})
4168

42-
const units = [
69+
const unitChannels = [
4370
academicUnit1,
4471
academicUnit2,
4572
academicUnit3,
@@ -63,29 +90,23 @@ describe("DepartmentListingPage", () => {
6390

6491
setMockResponse.get(
6592
urls.channels.counts("unit"),
66-
units.map((unit) => {
93+
unitChannels.map((channel) => {
6794
return {
68-
name: unit.code,
95+
name: channel.name,
6996
counts: {
70-
courses: courseCounts[unit.code],
71-
programs: programCounts[unit.code],
97+
courses: courseCounts[channel.name],
98+
programs: programCounts[channel.name],
7299
},
73100
}
74101
}),
75102
)
76-
setMockResponse.get(urls.offerors.list(), {
77-
count: units.length,
78-
results: units,
79-
})
80-
81-
units.forEach((unit) => {
82-
setMockResponse.get(urls.channels.details("unit", unit.code), {
83-
channel_url: `${window.location.origin}/units/${unit.code}`,
84-
})
103+
setMockResponse.get(urls.channels.list({ channel_type: "unit" }), {
104+
count: unitChannels.length,
105+
results: unitChannels,
85106
})
86107

87108
return {
88-
units,
109+
unitChannels,
89110
courseCounts,
90111
programCounts,
91112
}
@@ -98,7 +119,7 @@ describe("DepartmentListingPage", () => {
98119
})
99120

100121
it("Shows unit properties within the proper section", async () => {
101-
const { units, courseCounts, programCounts } = setupApis()
122+
const { unitChannels, courseCounts, programCounts } = setupApis()
102123

103124
renderWithProviders(<UnitsListingPage />)
104125

@@ -116,11 +137,15 @@ describe("DepartmentListingPage", () => {
116137
return links
117138
})
118139

119-
units.forEach((unit) => {
140+
unitChannels.forEach((channel) => {
141+
const { unit } = (channel as UnitChannel).unit_detail
120142
const section = unit.professional ? professionalSection : academicSection
121143
const card = within(section).getByTestId(`unit-card-${unit.code}`)
122144
const link = within(card).getByRole("link")
123-
expect(link).toHaveAttribute("href", `/units/${unit.code}`)
145+
expect(link).toHaveAttribute(
146+
"href",
147+
new URL(channel.channel_url!).pathname,
148+
)
124149

125150
const courseCount = courseCounts[unit.code]
126151
const programCount = programCounts[unit.code]

0 commit comments

Comments
 (0)