├── .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 | --------------------------------------------------------------------------------