Skip to content

Commit c953421

Browse files
committed
exercise 4 done
1 parent 0a44887 commit c953421

File tree

4 files changed

+97
-64
lines changed

4 files changed

+97
-64
lines changed

exercises/03.data-fetching/04.problem.infinite-fetching-with-fetchers/app/routes/_landing.products._index/route.tsx

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Filter, Grid, List } from 'lucide-react'
2-
import { useState } from 'react'
2+
import { useState, useRef } from 'react'
33
import { useSearchParams, useNavigation } from 'react-router'
44
import { ProductCard } from '#app/components/product-card.js'
55
import { getAllBrands } from '#app/domain/brand.server.js'
@@ -8,13 +8,15 @@ import {
88
extractProductFiltersFromSearchParams,
99
getProducts,
1010
} from '#app/domain/products.server.js'
11+
import { useIntersectionObserver } from '#app/hooks/use-intersection-observer.js'
1112
import {
1213
getMetaFromMatches,
1314
getMetaTitle,
1415
constructPrefixedTitle,
1516
} from '#app/utils/metadata.js'
1617
import { type Route } from './+types/route'
1718
import { ProductFilters } from './product-filters'
19+
import { useInfiniteProductFetcher } from './use-infinite-product-fetcher'
1820

1921
export const meta: Route.MetaFunction = ({ matches }) => {
2022
const rootMeta = getMetaFromMatches(matches, 'root')
@@ -49,11 +51,18 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
4951
export default function ProductsPage({ loaderData }: Route.ComponentProps) {
5052
const [searchParams, setSearchParams] = useSearchParams()
5153
const navigation = useNavigation()
54+
// 🐨 use the useInfiniteProductFetcher returned properties to fetch infinite data
55+
const { loadMoreProducts, allProducts, hasMore, isLoadingMore } =
56+
useInfiniteProductFetcher(loaderData)
5257
const isPageLoading = navigation.state !== 'idle'
58+
// 💣 We won't be needing this when we finish pagination
5359
const { products } = loaderData
5460
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
5561
const [showFilters, setShowFilters] = useState(false)
56-
62+
const observerRef = useRef<HTMLDivElement>(null)
63+
// 🐨 use the useIntersectionObserver to call a load of more products when end of screen is reached
64+
// 💰 pass in loadMoreProducts instead of the arrow function
65+
useIntersectionObserver(observerRef, () => {})
5766
return (
5867
<div className="min-h-screen bg-stone-50 dark:bg-gray-900">
5968
{/* Header */}
@@ -66,6 +75,7 @@ export default function ProductsPage({ loaderData }: Route.ComponentProps) {
6675
</h1>
6776
<p className="mt-2 text-gray-600 dark:text-gray-300">
6877
{loaderData.pagination.totalCount} products total (
78+
{/** 🐨 use the allProducts variable instead of products to show the number of allProducts */}
6979
{products.length} shown)
7080
</p>
7181
</div>
@@ -144,20 +154,50 @@ export default function ProductsPage({ loaderData }: Route.ComponentProps) {
144154

145155
{/* Products Grid/List */}
146156
<div className="flex-1">
147-
{viewMode === 'grid' ? (
148-
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
149-
{products.map((product) => (
150-
<ProductCard key={product.id} product={product} />
151-
))}
157+
{/** 🐨 we should show a loading UI when needed */}
158+
{/** 💰 use isPageLoading */}
159+
{false ? (
160+
<div className="mb-4 text-center text-gray-500 dark:text-gray-400">
161+
Loading...
152162
</div>
153163
) : (
154-
<div className="space-y-6">
155-
{products.map((product) => (
156-
<ProductCard key={product.id} product={product} />
157-
))}
158-
</div>
159-
)}
164+
<>
165+
<div
166+
className={
167+
viewMode === 'grid'
168+
? 'grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3'
169+
: 'space-y-6'
170+
}
171+
>
172+
{/** 🐨 we should use allProducts instead of products to show all fetched products */}
173+
{products.map((product) => (
174+
<ProductCard key={product.id} product={product} />
175+
))}
176+
</div>
177+
178+
{/* Intersection observer target */}
179+
{hasMore && (
180+
<div ref={observerRef} className="mt-8 flex justify-center">
181+
{isLoadingMore ? (
182+
<div className="text-center text-gray-500 dark:text-gray-400">
183+
<div className="inline-block h-6 w-6 animate-spin rounded-full border-2 border-amber-600 border-t-transparent"></div>
184+
<p className="mt-2">Loading more products...</p>
185+
</div>
186+
) : (
187+
<div className="h-4 w-full"></div>
188+
)}
189+
</div>
190+
)}
160191

192+
{/* End of results message */}
193+
{!hasMore && allProducts.length > 0 && (
194+
<div className="mt-8 text-center text-gray-500 dark:text-gray-400">
195+
<p>You've reached the end of the product list!</p>
196+
</div>
197+
)}
198+
</>
199+
)}
200+
{/** 🐨 we should use allProducts instead of products to show no products found */}
161201
{products.length === 0 && (
162202
<div className="py-16 text-center">
163203
<div className="mb-4 text-gray-400 dark:text-gray-600">

exercises/03.data-fetching/04.problem.infinite-fetching-with-fetchers/app/routes/_landing.products._index/use-infinite-product-fetcher.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,70 @@
11
import { useState, useEffect, useCallback } from "react"
22
import { useFetcher, useSearchParams, href } from "react-router"
3+
// 💰 use this to type the route
34
import { type Route } from "./+types/route"
45

56
export function useInfiniteProductFetcher(
6-
loaderData: Route.ComponentProps['loaderData'],
7+
// 🐨 we want to type the loader data to be the type of the returned loader data of the products listing page
8+
// 💰 Route.ComponentProps['loaderData']
9+
loaderData: any,
710
) {
8-
const fetcher = useFetcher<Route.ComponentProps['loaderData']>({
11+
// 💰 we are fetching from the same loader, so we can type this fetcher with the same type as loader-data
12+
const fetcher = useFetcher<any>({
913
key: 'infinite-product-fetcher',
1014
})
15+
// 💰 we are extracting the initial data we fetched and then we will load more from the server
1116
const { products, pagination } = loaderData
1217
const [allProducts, setAllProducts] = useState(products)
1318
const [currentPage, setCurrentPage] = useState(pagination.page)
1419
const [hasMore, setHasMore] = useState(pagination.hasMore)
1520
const [isLoadingMore, setIsLoadingMore] = useState(false)
1621
const [searchParams] = useSearchParams()
1722
useEffect(
23+
// 🐨 whenever we change the filters we should start from the beginning again
1824
function resetStateOnLoaderRefire() {
19-
//setAllProducts(products)
20-
// setCurrentPage(pagination.page)
21-
// setHasMore(pagination.hasMore)
25+
// 💰 whenever we reset the URL and the loader returns new products, we want to reset the state to the initial values
26+
// 💰 setAllProducts(products)
27+
// 💰 setCurrentPage(pagination.page)
28+
// 💰 setHasMore(pagination.hasMore)
29+
2230
},
2331
[pagination.hasMore, pagination.page, products],
2432
)
2533

2634
useEffect(
35+
36+
// 🐨 whenever we load the new products we want to append them to the existing array
2737
function setNewlyLoadedProducts() {
28-
if (fetcher.data && fetcher.state === 'idle' && isLoadingMore) {
29-
//setAllProducts((prev) => [...prev, ...(fetcher.data?.products ?? [])])
30-
// setCurrentPage(fetcher.data.pagination.page)
31-
//setHasMore(fetcher.data.pagination.hasMore)
32-
// setIsLoadingMore(false)
38+
// 💰 we want to make sure that the fetcher has returned some data and it's defined, and it is idle and we were loading more products
39+
if (false) {
40+
// 💰 we append the newly loaded products to the existing array
41+
// 💰 setAllProducts((prev) => [...prev, ...(fetcher.data.? ??[])])
42+
// 💰 we update the current page
43+
// 💰 setCurrentPage(fetcher.data.?)
44+
// 💰 we update the hasMore flag
45+
// 💰 setHasMore(fetcher.data.?)
46+
// 💰 we set isLoadingMore to false
47+
// 💰 setIsLoadingMore(false)
3348
}
3449
},
3550
[fetcher.data, fetcher.state, isLoadingMore],
3651
)
3752

3853
const loadMoreProducts = useCallback(() => {
39-
// setIsLoadingMore(true)
40-
// const nextPage = currentPage + 1
54+
// 🐨 we want to create the new search params so we can submit that to the loader so it gives us the next page of results
55+
// 💰 we set the flag to true
56+
// 💰 setIsLoadingMore(true)
57+
// 💰 we use the current page to create the next page
58+
// 💰 const nextPage = currentPage + 1
59+
60+
// 💰 Create URL params for the fetch
61+
// 💰 const params = new URLSearchParams(searchParams)
62+
// 💰 params.set('page', nextPage.toString())
4163

42-
// Create URL params for the fetch
43-
// const params = new URLSearchParams(searchParams)
44-
// params.set('page', nextPage.toString())
64+
// 🐨 finally we want to load the next page using the fetcher
65+
// 💰 use the `load` function and call the /products page in a type-safe manner (href function) and append the search params
66+
// 💰 + "?" + params.toString()
4567

46-
// void fetcher.load(href('/products') + '?' + params.toString())
4768
}, [currentPage, fetcher, searchParams])
4869

4970
return { fetcher, loadMoreProducts, allProducts, hasMore, isLoadingMore }

exercises/03.data-fetching/04.solution.infinite-fetching-with-fetchers copy/tsconfig.json

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
1-
Well done! You have successfully added metadata to your routes using the `meta` export.
2-
This allows you to set dynamic titles for each page in your application by merging
3-
the parent route's metadata with the current route's metadata.
1+
## Infinite Scrolling with `useFetcher` hook
42

5-
You have also ensured that all titles have a consistent prefix, "Epic Shop -", which
6-
improves the branding and recognition of your application.
3+
One more exercise to go! You have done an amazing job so far.
74

8-
Another benefit of the approach we just implemented is that if you add a new route
9-
and you forget to add a title there, it will fallback to the closest parent route's title,
10-
which is a nice safety net to have.
5+
In this one we tackled infinite scrolling using the `useFetcher` hook provided by React Router.
6+
We learned how to load data without navigating away from the current page, and how to use an intersection observer
7+
to detect when the user has scrolled to the bottom of the page.
118

12-
Now that you fully understand the `meta` export and how to use it effectively, let's
13-
move onto the next module when you're ready!
14-
15-
🧝‍♀️ I'm going to help you out for the next exercise, from what I understand it's all about
16-
data fetching and loaders, so I'm going to set up a database for you to use in the next exercise
17-
and also add some utilities to help you out with fetching data from the database! I hope
18-
this helps you out!
9+
Now all that is left to do for us it to fetch the data on the singular product page using loaders and we're done!
10+
1911

2012
You're doing great! 🚀

0 commit comments

Comments
 (0)