Skip to content

Commit 0a44887

Browse files
committed
add in exercise 4
1 parent 127376b commit 0a44887

File tree

56 files changed

+3746
-156
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+3746
-156
lines changed
Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1-
## Filtering, sorting and pagination
1+
## Infinite Scrolling with `useFetcher` hook
22

3-
Well done! You have successfully added filtering, sorting, and pagination to your product listing page.
4-
This allows your users to easily find and navigate through products based on their preferences.
3+
Now that we have everything in place, the lest step in improving our products listing page is to implement infinite scrolling.
4+
This will allow us to load more products as the user scrolls down the page, providing a seamless browsing experience.
55

6-
Now with that out of the way, you might've noticed that the pagination works, but it doesn't load more products!
7-
Let's fix that in the next exercise by using fetchers and infinite scrolling.
6+
### `useFetcher` hook
7+
The `useFetcher` hook from React Router is a powerful tool that allows you to perform data fetching and mutations without causing a full page navigation.
8+
When you call `useFetcher`, it returns an object that contains several properties and methods to help you manage data fetching.
9+
One of the key properties is `load`, which is a function that you can call to fetch data from a specified URL.
810

9-
🧝‍♀️ I'm going to help you out for the next exercise, I'll create an intersection observer hook to let us listen
10-
to when the user scrolls to the bottom of the page so we can load more products automatically! The rest is up to you!
11+
By using the `load` method, you can fetch additional data (like more products) and update your component's state accordingly. We will explore this
12+
hook further in upcoming workshops, but for now, it's important to understand that it allows us to fetch data without navigating away from the current page.
1113

12-
You're doing great! 🚀
14+
You typically use this hook when you don't want to add an event to the browsers `history` stack, for example when implementing infinite scrolling.
15+
16+
### Exercise Goals
17+
18+
- Add infinite scrolling to the products listing page using the `useFetcher` hook.
19+
- Use an intersection observer to detect when the user has scrolled to the bottom of the page.
20+
- Fetch and append more products to the existing list as the user scrolls down.
21+
- Ensure that the infinite scrolling works seamlessly with the existing filtering, sorting, and pagination features.
22+
- Provide feedback to the user when new products are being loaded (e.g., a loading spinner).
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect } from "react"
2+
3+
export const useIntersectionObserver = <T extends Element>(
4+
ref: React.RefObject<T | null>,
5+
onIntersect: () => void,
6+
options?: IntersectionObserverInit,
7+
) => {
8+
useEffect(() => {
9+
const observer = new IntersectionObserver((entries) => {
10+
entries.forEach((entry) => {
11+
if (entry.isIntersecting) {
12+
onIntersect()
13+
}
14+
})
15+
}, options)
16+
17+
const currentRef = ref.current
18+
if (currentRef) {
19+
observer.observe(currentRef)
20+
}
21+
22+
return () => {
23+
if (currentRef) {
24+
observer.unobserve(currentRef)
25+
}
26+
}
27+
}, [ref, onIntersect, options])
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useState, useEffect, useCallback } from "react"
2+
import { useFetcher, useSearchParams, href } from "react-router"
3+
import { type Route } from "./+types/route"
4+
5+
export function useInfiniteProductFetcher(
6+
loaderData: Route.ComponentProps['loaderData'],
7+
) {
8+
const fetcher = useFetcher<Route.ComponentProps['loaderData']>({
9+
key: 'infinite-product-fetcher',
10+
})
11+
const { products, pagination } = loaderData
12+
const [allProducts, setAllProducts] = useState(products)
13+
const [currentPage, setCurrentPage] = useState(pagination.page)
14+
const [hasMore, setHasMore] = useState(pagination.hasMore)
15+
const [isLoadingMore, setIsLoadingMore] = useState(false)
16+
const [searchParams] = useSearchParams()
17+
useEffect(
18+
function resetStateOnLoaderRefire() {
19+
//setAllProducts(products)
20+
// setCurrentPage(pagination.page)
21+
// setHasMore(pagination.hasMore)
22+
},
23+
[pagination.hasMore, pagination.page, products],
24+
)
25+
26+
useEffect(
27+
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)
33+
}
34+
},
35+
[fetcher.data, fetcher.state, isLoadingMore],
36+
)
37+
38+
const loadMoreProducts = useCallback(() => {
39+
// setIsLoadingMore(true)
40+
// const nextPage = currentPage + 1
41+
42+
// Create URL params for the fetch
43+
// const params = new URLSearchParams(searchParams)
44+
// params.set('page', nextPage.toString())
45+
46+
// void fetcher.load(href('/products') + '?' + params.toString())
47+
}, [currentPage, fetcher, searchParams])
48+
49+
return { fetcher, loadMoreProducts, allProducts, hasMore, isLoadingMore }
50+
}

exercises/03.data-fetching/04.problem.infinite-fetching-with-fetchers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "exercises_03.data-fetching_03.solution.filtering-and-pagination copy",
2+
"name": "exercises_03.data-fetching_04.problem.infinite-fetching-with-fetchers",
33
"private": true,
44
"type": "module",
55
"imports": {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"include": [
3+
"../05.solution.product-variations/**/*",
4+
"../05.solution.product-variations/**/.server/**/*",
5+
"../05.solution.product-variations/**/.client/**/*",
6+
"../05.solution.product-variations/.react-router/types/**/*"
7+
],
8+
"extends": ["@epic-web/config/typescript"],
9+
"compilerOptions": {
10+
"types": ["node", "vite/client"],
11+
"rootDirs": [
12+
"../05.solution.product-variations",
13+
"../05.solution.product-variations/.react-router/types"
14+
],
15+
"paths": {
16+
"#app/*": ["../05.solution.product-variations/app/*"]
17+
},
18+
"noUncheckedIndexedAccess": false
19+
}
20+
}

exercises/03.data-fetching/04.solution.infinite-fetching-with-fetchers/app/domain/products.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ function createProductWhereClause(filters?: ProductFilters): ProductWhereInput {
8989

9090
export async function getProducts(filters?: ProductFilters) {
9191
const page = filters?.page ?? 1;
92-
const limit = filters?.limit ?? 4; // Default to 9 products per page
92+
const limit = filters?.limit ?? 4;
9393
const skip = (page - 1) * limit;
9494

9595
const whereClause = createProductWhereClause(filters);

0 commit comments

Comments
 (0)