├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.js
├── Example1.js
├── Example2.js
├── Post.js
├── api
└── axios.js
├── hooks
└── usePosts.js
├── index.css
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # "React Infinite Scroll & Infinite Query Tutorial"
2 |
3 | ---
4 |
5 | ### Author Links
6 |
7 | 👋 Hello, I'm Dave Gray.
8 |
9 | ✅ [Check out my YouTube Channel with all of my tutorials](https://www.youtube.com/DaveGrayTeachesCode).
10 |
11 | 🚩 [Subscribe to my channel](https://bit.ly/3nGHmNn)
12 |
13 | ☕ [Buy Me A Coffee](https://buymeacoffee.com/DaveGray)
14 |
15 | 🚀 Follow Me:
16 |
17 | - [Twitter](https://twitter.com/yesdavidgray)
18 | - [LinkedIn](https://www.linkedin.com/in/davidagray/)
19 | - [Blog](https://yesdavidgray.com)
20 | - [Reddit](https://www.reddit.com/user/DaveOnEleven)
21 |
22 | ---
23 |
24 | ### Description
25 |
26 | 📺 [YouTube Video](https://youtu.be/JWlOcDus_rs) for this repository.
27 |
28 | ---
29 |
30 | ### 💻 Source Code
31 |
32 | - 🔗 [React Infinite Scroll & Infinite Query Tutorial - Completed Source Code](https://github.com/gitdagray/react_infinite_scroll)
33 |
34 | ---
35 |
36 | ### 🎓 Academic Honesty
37 |
38 | **DO NOT COPY FOR AN ASSIGNMENT** - Avoid plagiargism and adhere to the spirit of this [Academic Honesty Policy](https://www.freecodecamp.org/news/academic-honesty-policy/).
39 |
40 | ---
41 |
42 | ### 📚 Tutorial References
43 |
44 | - 🔗 [React Query: useInfiniteQuery](https://react-query-v2.tanstack.com/reference/useInfiniteQuery)
45 | - 🔗 [JSON Placeholder: Posts](https://jsonplaceholder.typicode.com/posts)
46 |
47 | ### ⚙ VS Code Extensions I Use:
48 |
49 | - 🔗 [ES7 React JS Snippets Extension](https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets)
50 | - 🔗 [vscode-icons VS Code Extension](https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons)
51 | - 🔗 [Github Themes VS Code Extension](https://marketplace.visualstudio.com/items?itemName=GitHub.github-vscode-theme)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_infinite_scroll",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.27.2",
7 | "react": "^18.2.0",
8 | "react-dom": "^18.2.0",
9 | "react-query": "^3.39.1",
10 | "react-scripts": "5.0.1"
11 | },
12 | "scripts": {
13 | "start": "react-scripts start",
14 | "build": "react-scripts build",
15 | "test": "react-scripts test",
16 | "eject": "react-scripts eject"
17 | },
18 | "eslintConfig": {
19 | "extends": [
20 | "react-app",
21 | "react-app/jest"
22 | ]
23 | },
24 | "browserslist": {
25 | "production": [
26 | ">0.2%",
27 | "not dead",
28 | "not op_mini all"
29 | ],
30 | "development": [
31 | "last 1 chrome version",
32 | "last 1 firefox version",
33 | "last 1 safari version"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_infinite_scroll/0e406e9f273ab2407f1872f62f5deebd6f55da03/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_infinite_scroll/0e406e9f273ab2407f1872f62f5deebd6f55da03/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitdagray/react_infinite_scroll/0e406e9f273ab2407f1872f62f5deebd6f55da03/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import Example1 from "./Example1";
2 | import Example2 from "./Example2";
3 |
4 | function App() {
5 | return
6 | }
7 |
8 | export default App;
9 |
--------------------------------------------------------------------------------
/src/Example1.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useCallback } from 'react'
2 | import usePosts from './hooks/usePosts'
3 | import Post from './Post'
4 |
5 | const Example1 = () => {
6 | const [pageNum, setPageNum] = useState(1)
7 | const {
8 | isLoading,
9 | isError,
10 | error,
11 | results,
12 | hasNextPage
13 | } = usePosts(pageNum)
14 |
15 | const intObserver = useRef()
16 | const lastPostRef = useCallback(post => {
17 | if (isLoading) return
18 |
19 | if (intObserver.current) intObserver.current.disconnect()
20 |
21 | intObserver.current = new IntersectionObserver(posts => {
22 | if (posts[0].isIntersecting && hasNextPage) {
23 | console.log('We are near the last post!')
24 | setPageNum(prev => prev + 1)
25 | }
26 | })
27 |
28 | if (post) intObserver.current.observe(post)
29 | }, [isLoading, hasNextPage])
30 |
31 | if (isError) return Error: {error.message}
32 |
33 | const content = results.map((post, i) => {
34 | if (results.length === i + 1) {
35 | return
36 | }
37 | return
38 | })
39 |
40 | return (
41 | <>
42 | ∞ Infinite Query & Scroll
∞ Ex. 1 - React only
43 | {content}
44 | {isLoading && Loading More Posts...
}
45 | Back to Top
46 | >
47 | )
48 | }
49 | export default Example1
--------------------------------------------------------------------------------
/src/Example2.js:
--------------------------------------------------------------------------------
1 | import { useRef, useCallback } from 'react'
2 | import Post from './Post'
3 | import { useInfiniteQuery } from 'react-query'
4 | import { getPostsPage } from './api/axios'
5 |
6 | const Example2 = () => {
7 |
8 | const {
9 | fetchNextPage, //function
10 | hasNextPage, // boolean
11 | isFetchingNextPage, // boolean
12 | data,
13 | status,
14 | error
15 | } = useInfiniteQuery('/posts', ({ pageParam = 1 }) => getPostsPage(pageParam), {
16 | getNextPageParam: (lastPage, allPages) => {
17 | return lastPage.length ? allPages.length + 1 : undefined
18 | }
19 | })
20 |
21 | const intObserver = useRef()
22 | const lastPostRef = useCallback(post => {
23 | if (isFetchingNextPage) return
24 |
25 | if (intObserver.current) intObserver.current.disconnect()
26 |
27 | intObserver.current = new IntersectionObserver(posts => {
28 | if (posts[0].isIntersecting && hasNextPage) {
29 | console.log('We are near the last post!')
30 | fetchNextPage()
31 | }
32 | })
33 |
34 | if (post) intObserver.current.observe(post)
35 | }, [isFetchingNextPage, fetchNextPage, hasNextPage])
36 |
37 | if (status === 'error') return Error: {error.message}
38 |
39 | const content = data?.pages.map(pg => {
40 | return pg.map((post, i) => {
41 | if (pg.length === i + 1) {
42 | return
43 | }
44 | return
45 | })
46 | })
47 |
48 | return (
49 | <>
50 | ∞ Infinite Query & Scroll
∞ Ex. 2 - React Query
51 | {content}
52 | {isFetchingNextPage && Loading More Posts...
}
53 | Back to Top
54 | >
55 | )
56 | }
57 | export default Example2
--------------------------------------------------------------------------------
/src/Post.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Post = React.forwardRef(({ post }, ref) => {
4 |
5 | const postBody = (
6 | <>
7 | {post.title}
8 | {post.body}
9 | Post ID: {post.id}
10 | >
11 | )
12 |
13 | const content = ref
14 | ? {postBody}
15 | : {postBody}
16 |
17 | return content
18 | })
19 |
20 | export default Post
--------------------------------------------------------------------------------
/src/api/axios.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export const api = axios.create({
4 | baseURL: 'https://jsonplaceholder.typicode.com'
5 | })
6 |
7 | export const getPostsPage = async (pageParam = 1, options = {}) => {
8 | const response = await api.get(`/posts?_page=${pageParam}`, options)
9 | return response.data
10 | }
--------------------------------------------------------------------------------
/src/hooks/usePosts.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { getPostsPage } from '../api/axios'
3 |
4 | const usePosts = (pageNum = 1) => {
5 | const [results, setResults] = useState([])
6 | const [isLoading, setIsLoading] = useState(false)
7 | const [isError, setIsError] = useState(false)
8 | const [error, setError] = useState({})
9 | const [hasNextPage, setHasNextPage] = useState(false)
10 |
11 | useEffect(() => {
12 | setIsLoading(true)
13 | setIsError(false)
14 | setError({})
15 |
16 | const controller = new AbortController()
17 | const { signal } = controller
18 |
19 | getPostsPage(pageNum, { signal })
20 | .then(data => {
21 | setResults(prev => [...prev, ...data])
22 | setHasNextPage(Boolean(data.length))
23 | setIsLoading(false)
24 | })
25 | .catch(e => {
26 | setIsLoading(false)
27 | if (signal.aborted) return
28 | setIsError(true)
29 | setError({ message: e.message })
30 | })
31 |
32 | return () => controller.abort()
33 |
34 | }, [pageNum])
35 |
36 | return { isLoading, isError, error, results, hasNextPage }
37 | }
38 |
39 | export default usePosts
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Nunito&display=swap");
2 |
3 | * {
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | }
8 |
9 | html {
10 | scroll-behavior: smooth;
11 | font-size: 1.5rem;
12 | }
13 |
14 | body {
15 | background-color: #000;
16 | font-family: 'Nunito', sans-serif;
17 | }
18 |
19 | p {
20 | color: whitesmoke;
21 | margin: 1em 0 0;
22 | }
23 |
24 | article {
25 | margin: 1em;
26 | padding: 1em;
27 | background-color: rebeccapurple;
28 | color: whitesmoke;
29 | border-radius: 15px;
30 | }
31 |
32 | h1 {
33 | margin: 1rem;
34 | color: whitesmoke;
35 | }
36 |
37 | h2::first-letter,
38 | p::first-letter {
39 | text-transform: uppercase;
40 | }
41 |
42 | .center {
43 | text-align: center;
44 | margin-bottom: 1em;
45 | }
46 |
47 | a:any-link {
48 | color: whitesmoke;
49 | }
50 |
51 | a:hover,
52 | a:focus-within {
53 | color: hsla(0, 0%, 96%, 0.9);
54 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | import { QueryClient, QueryClientProvider } from 'react-query'
7 |
8 | const queryClient = new QueryClient()
9 |
10 | const root = ReactDOM.createRoot(document.getElementById('root'));
11 | root.render(
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------