Skip to content

Commit a8204de

Browse files
committed
Refactor to use a single useQueries hook between renders
1 parent 96b0391 commit a8204de

File tree

3 files changed

+68
-124
lines changed

3 files changed

+68
-124
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,13 @@ const useSchoolsList = () => {
427427
return useQuery(learningResources.schools())
428428
}
429429

430+
/*
431+
* Not intended to be imported except for special cases.
432+
* It's used in the ResourceCarousel to dynamically build a single useQueries hook
433+
* from config because a React component cannot conditionally call hooks during renders.
434+
*/
435+
export { default as learningResourcesKeyFactory } from "./keyFactory"
436+
430437
export {
431438
useLearningResourcesList,
432439
useFeaturedLearningResourcesList,

frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.tsx

Lines changed: 47 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from "react"
22
import {
33
useFeaturedLearningResourcesList,
4-
useLearningResourcesList,
5-
useLearningResourcesSearch,
4+
learningResourcesKeyFactory,
65
} from "api/hooks/learningResources"
6+
77
import {
88
Carousel,
99
TabButton,
@@ -13,14 +13,10 @@ import {
1313
styled,
1414
Typography,
1515
} from "ol-components"
16-
import type {
17-
TabConfig,
18-
ResourceDataSource,
19-
SearchDataSource,
20-
FeaturedDataSource,
21-
} from "./types"
16+
import type { TabConfig, FeaturedDataSource } from "./types"
2217
import { LearningResource } from "api"
2318
import { ResourceCard } from "../ResourceCard/ResourceCard"
19+
import { useQueries } from "@tanstack/react-query"
2420

2521
const StyledCarousel = styled(Carousel)({
2622
/**
@@ -40,92 +36,13 @@ const StyledCarousel = styled(Carousel)({
4036
},
4137
})
4238

43-
type DataPanelProps<T extends TabConfig["data"] = TabConfig["data"]> = {
44-
dataConfig: T
45-
isLoading?: boolean
46-
children: ({
47-
resources,
48-
childrenLoading,
49-
}: {
50-
resources: LearningResource[]
51-
childrenLoading: boolean
52-
}) => React.ReactNode
53-
}
54-
5539
type LoadTabButtonProps = {
5640
config: FeaturedDataSource
5741
label: React.ReactNode
5842
key: number
5943
value: string
6044
}
6145

62-
const ResourcesData: React.FC<DataPanelProps<ResourceDataSource>> = ({
63-
dataConfig,
64-
children,
65-
}) => {
66-
const { data, isLoading } = useLearningResourcesList(dataConfig.params)
67-
return children({
68-
resources: data?.results ?? [],
69-
childrenLoading: isLoading,
70-
})
71-
}
72-
73-
const SearchData: React.FC<DataPanelProps<SearchDataSource>> = ({
74-
dataConfig,
75-
children,
76-
}) => {
77-
const { data, isLoading } = useLearningResourcesSearch(dataConfig.params)
78-
return children({
79-
resources: data?.results ?? [],
80-
childrenLoading: isLoading,
81-
})
82-
}
83-
84-
const FeaturedData: React.FC<DataPanelProps<FeaturedDataSource>> = ({
85-
dataConfig,
86-
children,
87-
}) => {
88-
const { data, isLoading } = useFeaturedLearningResourcesList(
89-
dataConfig.params,
90-
)
91-
return children({
92-
resources: data?.results ?? [],
93-
childrenLoading: isLoading,
94-
})
95-
}
96-
97-
/**
98-
* A wrapper to load data based `TabConfig.data`.
99-
*
100-
* For each `TabConfig.data.type`, a different API endpoint, and hence
101-
* react-query hook, is used. Since hooks can't be called conditionally within
102-
* a single component, each type of data is handled in a separate component.
103-
*/
104-
const DataPanel: React.FC<DataPanelProps> = ({
105-
dataConfig,
106-
isLoading,
107-
children,
108-
}) => {
109-
if (!isLoading) {
110-
switch (dataConfig.type) {
111-
case "resources":
112-
return <ResourcesData dataConfig={dataConfig}>{children}</ResourcesData>
113-
case "lr_search":
114-
return <SearchData dataConfig={dataConfig}>{children}</SearchData>
115-
case "lr_featured":
116-
return <FeaturedData dataConfig={dataConfig}>{children}</FeaturedData>
117-
default:
118-
// @ts-expect-error This will always be an error if the switch statement
119-
// is exhaustive since dataConfig will have type `never`
120-
throw new Error(`Unknown data type: ${dataConfig.type}`)
121-
}
122-
} else
123-
return children({
124-
resources: [],
125-
childrenLoading: true,
126-
})
127-
}
128-
12946
/**
13047
* Tab button that loads the resource, so we can determine if it needs to be
13148
* displayed or not. This shouldn't cause double-loading since React Query
@@ -195,48 +112,45 @@ const TabsList = styled(TabButtonList)({
195112

196113
type ContentProps = {
197114
resources: LearningResource[]
198-
isLoading?: boolean
115+
childrenLoading?: boolean
199116
tabConfig: TabConfig
200117
}
201118

202119
type PanelChildrenProps = {
203120
config: TabConfig[]
121+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
122+
queries: any
204123
children: (props: ContentProps) => React.ReactNode
205-
isLoading?: boolean
206124
}
207125
const PanelChildren: React.FC<PanelChildrenProps> = ({
208126
config,
127+
queries,
209128
children,
210-
isLoading,
211129
}) => {
212130
if (config.length === 1) {
213-
return (
214-
<DataPanel dataConfig={config[0].data} isLoading={isLoading}>
215-
{({ resources, childrenLoading }) =>
216-
children({
217-
resources,
218-
isLoading: childrenLoading || isLoading,
219-
tabConfig: config[0],
220-
})
221-
}
222-
</DataPanel>
223-
)
131+
const { data, isLoading } = queries[0]
132+
const resources = data?.results ?? []
133+
return children({
134+
resources,
135+
childrenLoading: isLoading,
136+
tabConfig: config[0],
137+
})
224138
}
225139
return (
226140
<>
227-
{config.map((tabConfig, index) => (
228-
<StyledTabPanel key={index} value={index.toString()}>
229-
<DataPanel dataConfig={tabConfig.data} isLoading={isLoading}>
230-
{({ resources, childrenLoading }) =>
231-
children({
232-
resources,
233-
isLoading: childrenLoading || isLoading,
234-
tabConfig,
235-
})
236-
}
237-
</DataPanel>
238-
</StyledTabPanel>
239-
))}
141+
{config.map((tabConfig, index) => {
142+
const { data, isLoading } = queries[index]
143+
const resources = data?.results ?? []
144+
return (
145+
<StyledTabPanel key={index} value={index.toString()}>
146+
{children({
147+
resources,
148+
childrenLoading: isLoading,
149+
tabConfig,
150+
})}
151+
</StyledTabPanel>
152+
)
153+
})}
240154
</>
241155
)
242156
}
@@ -258,6 +172,7 @@ type ResourceCarouselProps = {
258172
title: string
259173
className?: string
260174
isLoading?: boolean
175+
"data-testid"?: string
261176
}
262177
/**
263178
* A tabbed carousel that fetches resources based on the configuration provided.
@@ -275,12 +190,27 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
275190
title,
276191
className,
277192
isLoading,
193+
"data-testid": dataTestId,
278194
}) => {
279195
const [tab, setTab] = React.useState("0")
280196
const [ref, setRef] = React.useState<HTMLDivElement | null>(null)
281197

198+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
199+
const queries = useQueries<any>({
200+
queries: config.map((tab) => {
201+
switch (tab.data.type) {
202+
case "resources":
203+
return learningResourcesKeyFactory.list(tab.data.params)
204+
case "lr_search":
205+
return learningResourcesKeyFactory.search(tab.data.params)
206+
case "lr_featured":
207+
return learningResourcesKeyFactory.featured(tab.data.params)
208+
}
209+
}),
210+
})
211+
282212
return (
283-
<MobileOverflow className={className}>
213+
<MobileOverflow className={className} data-testid={dataTestId}>
284214
<TabContext value={tab}>
285215
<HeaderRow>
286216
<HeaderText variant="h4">{title}</HeaderText>
@@ -309,8 +239,8 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
309239
</ControlsContainer>
310240
) : null}
311241
</HeaderRow>
312-
<PanelChildren config={config} isLoading={isLoading}>
313-
{({ resources, isLoading: childrenLoading, tabConfig }) => (
242+
<PanelChildren config={config} queries={queries}>
243+
{({ resources, childrenLoading, tabConfig }) => (
314244
<StyledCarousel arrowsContainer={ref}>
315245
{isLoading || childrenLoading
316246
? Array.from({ length: 6 }).map((_, index) => (

frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,13 @@ const CarouselContainer = styled.div(({ theme }) => ({
259259
},
260260
}))
261261

262+
const StyledResourceCarousel = styled(ResourceCarousel)(({ theme }) => ({
263+
padding: "40px 0",
264+
[theme.breakpoints.down("md")]: {
265+
padding: "16px 0",
266+
},
267+
}))
268+
262269
interface UserMenuTabProps {
263270
icon: React.ReactNode
264271
text: string
@@ -487,16 +494,16 @@ const DashboardPage: React.FC = () => {
487494
title={"Courses with Certificates"}
488495
isLoading={isLoadingProfile}
489496
config={CERTIFICATE_COURSES_CAROUSEL}
497+
data-testid="certification-carousel"
490498
/>
491499
</CarouselContainer>
492500
) : (
493-
<CarouselContainer data-testid="free-carousel">
494-
<ResourceCarousel
495-
title={"Free courses"}
496-
isLoading={isLoadingProfile}
497-
config={FREE_COURSES_CAROUSEL}
498-
/>
499-
</CarouselContainer>
501+
<StyledResourceCarousel
502+
title="Free courses"
503+
isLoading={isLoadingProfile}
504+
config={FREE_COURSES_CAROUSEL}
505+
data-testid="free-carousel"
506+
/>
500507
)}
501508
<CarouselContainer data-testid="new-learning-resources-carousel">
502509
<ResourceCarousel

0 commit comments

Comments
 (0)