Skip to content

Commit b44f786

Browse files
committed
feat: implement lazy loading for cards using IntersectionObserver and refactor related components for improved performance
1 parent d58d4d1 commit b44f786

15 files changed

Lines changed: 367 additions & 206 deletions

File tree

src/components/Elements/Card/Card.tsx

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,64 +11,68 @@ type RootCardProps = CardPropsType & {
1111
settingsComponent?: React.ReactNode
1212
fullBlock?: boolean
1313
}
14+
export const Card = React.forwardRef<HTMLDivElement, RootCardProps>(
15+
(
16+
{
17+
meta,
18+
titleComponent,
19+
settingsComponent,
20+
className,
21+
withAds = false,
22+
children,
23+
fullBlock = false,
24+
knob,
25+
},
26+
ref
27+
) => {
28+
const { icon, label, badge } = meta
29+
const [canAdsLoad, setCanAdsLoad] = useState(true)
1430

15-
export const Card = ({
16-
meta,
17-
titleComponent,
18-
settingsComponent,
19-
className,
20-
withAds = false,
21-
children,
22-
fullBlock = false,
23-
knob,
24-
}: RootCardProps) => {
25-
const { icon, label, badge } = meta
26-
const [canAdsLoad, setCanAdsLoad] = useState(true)
27-
28-
useEffect(() => {
29-
if (!withAds) {
30-
return
31-
}
32-
33-
const handleClassChange = () => {
34-
if (document.documentElement.classList.contains('dndState')) {
35-
setCanAdsLoad(false)
36-
} else {
37-
setCanAdsLoad(true)
31+
useEffect(() => {
32+
if (!withAds) {
33+
return
3834
}
39-
}
4035

41-
const observer = new MutationObserver(handleClassChange)
42-
observer.observe(document.documentElement, { attributes: true })
36+
const handleClassChange = () => {
37+
if (document.documentElement.classList.contains('dndState')) {
38+
setCanAdsLoad(false)
39+
} else {
40+
setCanAdsLoad(true)
41+
}
42+
}
4343

44-
return () => {
45-
observer.disconnect()
46-
}
47-
}, [withAds])
44+
const observer = new MutationObserver(handleClassChange)
45+
observer.observe(document.documentElement, { attributes: true })
4846

49-
return (
50-
<div className={clsx('block', fullBlock && 'fullBlock', className)}>
51-
<MobileBreakpoint>
52-
{settingsComponent && <button className="floatingFilter">{settingsComponent}</button>}
53-
</MobileBreakpoint>
54-
<div className="blockHeader">
55-
{knob}
56-
<span className="blockHeaderIcon">{icon}</span> {titleComponent || label}{' '}
57-
<DesktopBreakpoint>
58-
{settingsComponent && (
59-
<span className="blockHeaderSettingsButton">{settingsComponent}</span>
60-
)}
61-
</DesktopBreakpoint>
62-
{badge && <span className="blockHeaderBadge">{badge}</span>}
63-
</div>
47+
return () => {
48+
observer.disconnect()
49+
}
50+
}, [withAds])
6451

65-
{canAdsLoad && withAds && (
66-
<div className="ad-wrapper blockRow">
67-
<AdvBanner />
52+
return (
53+
<div ref={ref} className={clsx('block', fullBlock && 'fullBlock', className)}>
54+
<MobileBreakpoint>
55+
{settingsComponent && <button className="floatingFilter">{settingsComponent}</button>}
56+
</MobileBreakpoint>
57+
<div className="blockHeader">
58+
{knob}
59+
<span className="blockHeaderIcon">{icon}</span> {titleComponent || label}{' '}
60+
<DesktopBreakpoint>
61+
{settingsComponent && (
62+
<span className="blockHeaderSettingsButton">{settingsComponent}</span>
63+
)}
64+
</DesktopBreakpoint>
65+
{badge && <span className="blockHeaderBadge">{badge}</span>}
6866
</div>
69-
)}
7067

71-
<div className="blockContent scrollable">{children}</div>
72-
</div>
73-
)
74-
}
68+
{canAdsLoad && withAds && (
69+
<div className="ad-wrapper blockRow">
70+
<AdvBanner />
71+
</div>
72+
)}
73+
74+
<div className="blockContent scrollable">{children}</div>
75+
</div>
76+
)
77+
}
78+
)

src/features/cards/components/aiCard/AICard.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,46 @@
1+
import { useCallback, useMemo } from 'react'
12
import { Card } from 'src/components/Elements'
23
import { ListComponent } from 'src/components/List'
34
import { FeedItem, useGetFeed } from 'src/features/feed'
45
import { useUserPreferences } from 'src/stores/preferences'
56
import { CardPropsType, FeedItemData } from 'src/types'
6-
import { CardSettings } from '../CardSettings'
7+
import { useShallow } from 'zustand/shallow'
8+
import { useLazyListLoad } from '../../hooks/useLazyListLoad'
9+
import { MemoizedCardSettings } from '../CardSettings'
710

811
export function AICard(props: CardPropsType) {
912
const { meta } = props
10-
const { userSelectedTags } = useUserPreferences()
13+
const userSelectedTags = useUserPreferences(useShallow((state) => state.userSelectedTags))
14+
const { ref, isVisible } = useLazyListLoad()
15+
const queryTags = useMemo(
16+
() => userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()),
17+
[userSelectedTags]
18+
)
19+
1120
const {
1221
data: articles,
1322
isLoading,
1423
error,
1524
} = useGetFeed({
16-
tags: userSelectedTags.map((tag) => tag.label.toLocaleLowerCase()),
25+
tags: queryTags,
1726
config: {
1827
cacheTime: 0,
1928
staleTime: 0,
2029
useErrorBoundary: false,
30+
enabled: isVisible,
2131
},
2232
})
2333

24-
const renderItem = (item: FeedItemData, index: number) => (
25-
<FeedItem item={item} key={`ai-${index}`} index={index} analyticsTag={meta.analyticsTag} />
34+
const renderItem = useCallback(
35+
(item: FeedItemData) => <FeedItem item={item} key={item.id} analyticsTag={meta.analyticsTag} />,
36+
[meta.analyticsTag]
2637
)
2738

2839
return (
29-
<Card settingsComponent={<CardSettings url={meta.link} id={meta.value} />} {...props}>
40+
<Card
41+
ref={ref}
42+
settingsComponent={<MemoizedCardSettings url={meta.link} id={meta.value} />}
43+
{...props}>
3044
<ListComponent<FeedItemData>
3145
items={articles?.pages.flatMap((page) => page.data) || []}
3246
error={error}

src/features/cards/components/conferencesCard/ConferencesCard.tsx

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,53 @@
1+
import { useCallback } from 'react'
12
import { Card } from 'src/components/Elements'
23
import { ListConferenceComponent } from 'src/components/List/ListConferenceComponent'
34
import { CardPropsType, Conference } from 'src/types'
45
import { useGetConferences } from '../../api/getConferences'
6+
import { useLazyListLoad } from '../../hooks/useLazyListLoad'
57
import { useSelectedTags } from '../../hooks/useSelectedTags'
6-
import { CardHeader } from '../CardHeader'
7-
import { CardSettings } from '../CardSettings'
8+
import { MemoizedCardHeader } from '../CardHeader'
9+
import { MemoizedCardSettings } from '../CardSettings'
810
import ConferenceItem from './ConferenceItem'
911

10-
const GLOBAL_TAG = { label: 'Global', value: 'tech' }
12+
const GLOBAL_TAG = { label: 'Global', value: 'general' }
1113
export function ConferencesCard(props: CardPropsType) {
1214
const { meta } = props
13-
const { queryTags, selectedTag, cardSettings } = useSelectedTags({
15+
const { ref, isVisible } = useLazyListLoad()
16+
const {
17+
queryTags,
18+
selectedTag,
19+
cardSettings: { sortBy, language } = {},
20+
} = useSelectedTags({
1421
source: meta.value,
1522
fallbackTag: GLOBAL_TAG,
1623
})
1724
const { isLoading, data: results } = useGetConferences({
18-
tags: queryTags.map((tag) => tag.value),
25+
tags: queryTags,
26+
config: {
27+
enabled: isVisible,
28+
},
1929
})
2030

21-
const renderItem = (item: Conference, index: number) => (
22-
<ConferenceItem item={item} key={item.id} index={index} analyticsTag={meta.analyticsTag} />
31+
const renderItem = useCallback(
32+
(item: Conference) => (
33+
<ConferenceItem item={item} key={item.id} analyticsTag={meta.analyticsTag} />
34+
),
35+
[meta.analyticsTag]
2336
)
2437

2538
return (
2639
<Card
40+
ref={ref}
2741
{...props}
2842
titleComponent={
29-
<CardHeader label={meta.label} fallbackTag={GLOBAL_TAG} selectedTag={selectedTag} />
43+
<MemoizedCardHeader label={meta.label} fallbackTag={GLOBAL_TAG} selectedTag={selectedTag} />
3044
}
3145
settingsComponent={
32-
<CardSettings
46+
<MemoizedCardSettings
3347
url={meta.link}
3448
id={meta.value}
35-
sortBy={cardSettings?.sortBy}
36-
language={cardSettings?.language}
49+
sortBy={sortBy}
50+
language={language}
3751
sortOptions={[
3852
{
3953
label: 'Upcoming',
@@ -43,7 +57,7 @@ export function ConferencesCard(props: CardPropsType) {
4357
/>
4458
}>
4559
<ListConferenceComponent
46-
sortBy={cardSettings?.sortBy as keyof Conference}
60+
sortBy={sortBy as keyof Conference}
4761
items={results}
4862
isLoading={isLoading}
4963
renderItem={renderItem}

src/features/cards/components/devtoCard/DevtoCard.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Card } from 'src/components/Elements'
55
import { ListPostComponent } from 'src/components/List/ListPostComponent'
66
import { Article, CardPropsType } from 'src/types'
77
import { useGetSourceArticles } from '../../api/getSourceArticles'
8+
import { useLazyListLoad } from '../../hooks/useLazyListLoad'
89
import { useSelectedTags } from '../../hooks/useSelectedTags'
910
import { MemoizedCardHeader } from '../CardHeader'
1011
import { MemoizedCardSettings } from '../CardSettings'
@@ -14,6 +15,7 @@ const GLOBAL_TAG = { label: 'Global', value: 'programming' }
1415

1516
export function DevtoCard(props: CardPropsType) {
1617
const { meta } = props
18+
const { ref, isVisible } = useLazyListLoad()
1719

1820
const {
1921
queryTags,
@@ -31,6 +33,9 @@ export function DevtoCard(props: CardPropsType) {
3133
} = useGetSourceArticles({
3234
source: 'devto',
3335
tags: queryTags,
36+
config: {
37+
enabled: isVisible,
38+
},
3439
})
3540

3641
const renderItem = useCallback(
@@ -40,6 +45,7 @@ export function DevtoCard(props: CardPropsType) {
4045

4146
return (
4247
<Card
48+
ref={ref}
4349
titleComponent={
4450
<MemoizedCardHeader label={meta.label} fallbackTag={GLOBAL_TAG} selectedTag={selectedTag} />
4551
}

src/features/cards/components/freecodecampCard/FreecodecampCard.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Card } from 'src/components/Elements'
33
import { ListPostComponent } from 'src/components/List/ListPostComponent'
44
import { Article, CardPropsType } from 'src/types'
55
import { useGetSourceArticles } from '../../api/getSourceArticles'
6+
import { useLazyListLoad } from '../../hooks/useLazyListLoad'
67
import { useSelectedTags } from '../../hooks/useSelectedTags'
78
import { MemoizedCardHeader } from '../CardHeader'
89
import { MemoizedCardSettings } from '../CardSettings'
@@ -12,6 +13,7 @@ const GLOBAL_TAG = { label: 'Global', value: 'global' }
1213

1314
export function FreecodecampCard(props: CardPropsType) {
1415
const { meta } = props
16+
const { ref, isVisible } = useLazyListLoad()
1517
const {
1618
queryTags,
1719
selectedTag,
@@ -24,6 +26,9 @@ export function FreecodecampCard(props: CardPropsType) {
2426
const { data, isLoading } = useGetSourceArticles({
2527
source: 'freecodecamp',
2628
tags: queryTags,
29+
config: {
30+
enabled: isVisible,
31+
},
2732
})
2833

2934
const renderItem = useCallback(
@@ -33,6 +38,7 @@ export function FreecodecampCard(props: CardPropsType) {
3338

3439
return (
3540
<Card
41+
ref={ref}
3642
titleComponent={
3743
<MemoizedCardHeader label={meta.label} fallbackTag={GLOBAL_TAG} selectedTag={selectedTag} />
3844
}

0 commit comments

Comments
 (0)