├── .eslintrc.json ├── next.config.js ├── public ├── favicon.ico └── vercel.svg ├── pages ├── api │ └── hello.js ├── _app.js ├── index.js ├── infiniteCSR.js ├── paginationCSR.js └── paginationSSR.js ├── .gitignore ├── package.json ├── README.md └── styles ├── globals.css └── Home.module.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elisabeth-leonhardt/react-query-pagination/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pagination", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev -p 3001", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@material-ui/core": "^4.12.3", 12 | "@material-ui/lab": "^4.0.0-alpha.60", 13 | "next": "12.0.4", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2", 16 | "react-infinite-scroll-component": "^6.1.0", 17 | "react-query": "^3.33.5" 18 | }, 19 | "devDependencies": { 20 | "eslint": "7.32.0", 21 | "eslint-config-next": "12.0.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { ReactQueryDevtools } from "react-query/devtools"; 3 | 4 | import { Hydrate, QueryClient, QueryClientProvider } from "react-query"; 5 | import { useState } from "react"; 6 | 7 | function MyApp({ Component, pageProps }) { 8 | const [queryClient] = useState(() => new QueryClient()); 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default MyApp; 20 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | Install the dependencies (I use npm but use whatever you like here) 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | Run the project 12 | 13 | ```bash 14 | npm run dev 15 | # or 16 | yarn dev 17 | ``` 18 | 19 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. 20 | 21 | ## This project implements Pagination and Infinite Scroll with the Rick and Morty API 22 | 23 | to see the implemented pagination with CSR, go to [http://localhost:3001/paginationCSR](http://localhost:3001/paginationCSR) 24 | 25 | to see the implemented pagination with SSR, go to [http://localhost:3001/paginationSSR](http://localhost:3001/paginationSSR) 26 | 27 | to see the implemented infinite scroll (CSR), go to [http://localhost:3001/infiniteCSR](http://localhost:3001/infiniteCSR) 28 | 29 | don't forget to check out my [blog](https://dev.to/elisabethleonhardt) on dev.to to read the explainations on how this is implemented! 30 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function Home(props) { 4 | return ( 5 |
6 |

Explore how I implemented pagination:

7 |

Client side pagination:

8 | 9 | Read the Article 10 | 11 | 12 | See it working 13 | {" "} 14 | 15 | See the code 16 | 17 |

Server side pagination:

18 | Read the Article 19 | 20 | See it working 21 | {" "} 22 | 23 | See the code 24 | 25 |

Explore how I implemented an infinite scroll:

26 | 27 | Read the Article 28 | 29 | 30 | See it working 31 | {" "} 32 | 33 | See the code 34 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | } 16 | 17 | h2 { 18 | padding-top: 1rem; 19 | } 20 | 21 | #welcome { 22 | max-width: 1300px; 23 | margin: auto; 24 | } 25 | 26 | #welcome a { 27 | border: 2px solid black; 28 | border-radius: 10px; 29 | padding: 1rem; 30 | margin-right: 1rem; 31 | color: black; 32 | text-decoration: none; 33 | } 34 | .grid-container { 35 | display: grid; 36 | grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr)); 37 | gap: 2rem; 38 | max-width: 1300px; 39 | width: 100%; 40 | margin: auto; 41 | padding: 2rem; 42 | } 43 | 44 | article { 45 | display: flex; 46 | flex-direction: column; 47 | align-items: center; 48 | text-align: center; 49 | border-radius: 0.5em; 50 | overflow: hidden; 51 | box-shadow: rgba(99, 99, 99, 0.5) 0px 2px 8px 0px; 52 | padding-bottom: 1rem; 53 | } 54 | 55 | article > *:not(img) { 56 | padding-inline: 1rem; 57 | } 58 | 59 | article img { 60 | object-fit: cover; 61 | } 62 | 63 | .pagination { 64 | max-width: 1300px; 65 | width: 100%; 66 | margin: auto; 67 | justify-content: center; 68 | display: flex; 69 | } 70 | 71 | .pagination:nth-of-type(2) { 72 | margin-bottom: 2rem; 73 | } 74 | -------------------------------------------------------------------------------- /pages/infiniteCSR.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import InfiniteScroll from "react-infinite-scroll-component"; 3 | import { useInfiniteQuery } from "react-query"; 4 | 5 | function InfiniteCSRPage() { 6 | const { data, status, fetchNextPage, hasNextPage } = useInfiniteQuery( 7 | "infiniteCharacters", 8 | async ({ pageParam = 1 }) => 9 | await fetch( 10 | `https://rickandmortyapi.com/api/character/?page=${pageParam}` 11 | ).then((result) => result.json()), 12 | { 13 | getNextPageParam: (lastPage, pages) => { 14 | if (lastPage.info.next) { 15 | return pages.length + 1; 16 | } 17 | }, 18 | } 19 | ); 20 | return ( 21 |
22 |

23 | Rick and Morty with React Query and Infinite Scroll - Client Side 24 | Rendered 25 |

26 | {status === "success" && ( 27 | Loading...} 32 | > 33 |
34 | {data?.pages.map((page) => ( 35 | <> 36 | {page.results.map((character) => ( 37 |
38 | {character.name} 45 |
46 |

Name: {character.name}

47 |

Lives in: {character.location.name}

48 |

Species: {character.species}

49 | Id: {character.id} 50 |
51 |
52 | ))} 53 | 54 | ))} 55 |
56 |
57 | )} 58 |
59 | ); 60 | } 61 | 62 | export default InfiniteCSRPage; 63 | -------------------------------------------------------------------------------- /pages/paginationCSR.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from "react-query"; 2 | import Pagination from "@material-ui/lab/Pagination"; 3 | import { useEffect, useState } from "react"; 4 | import { useRouter } from "next/router"; 5 | 6 | export default function PaginationPage(props) { 7 | const router = useRouter(); 8 | const [page, setPage] = useState(1); 9 | const { data } = useQuery( 10 | ["characters", page], 11 | async () => 12 | await fetch( 13 | `https://rickandmortyapi.com/api/character/?page=${page}` 14 | ).then((result) => result.json()), 15 | { 16 | keepPreviousData: true, 17 | } 18 | ); 19 | function handlePaginationChange(e, value) { 20 | setPage(value); 21 | router.push(`paginationCSR/?page=${value}`, undefined, { shallow: true }); 22 | } 23 | useEffect(() => { 24 | if (router.query.page) { 25 | setPage(parseInt(router.query.page)); 26 | } 27 | }, [router.query.page]); 28 | return ( 29 |
30 |

31 | Rick and Morty with React Query and Pagination - Client Side Rendered 32 |

33 | 41 |
42 | {data?.results?.map((character) => ( 43 |
44 | {character.name} 51 |
52 |

Name: {character.name}

53 |

Lives in: {character.location.name}

54 |

Species: {character.species}

55 | Id: {character.id} 56 |
57 |
58 | ))} 59 |
60 | 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pages/paginationSSR.js: -------------------------------------------------------------------------------- 1 | import { useQuery, dehydrate, QueryClient } from "react-query"; 2 | import Pagination from "@material-ui/lab/Pagination"; 3 | import { useState } from "react"; 4 | import { useRouter } from "next/router"; 5 | 6 | export default function paginationSSR(props) { 7 | const router = useRouter(); 8 | const [page, setPage] = useState(parseInt(router.query.page) || 1); 9 | const { data } = useQuery( 10 | ["characters", page], 11 | async () => 12 | await fetch( 13 | `https://rickandmortyapi.com/api/character/?page=${page}` 14 | ).then((result) => result.json()), 15 | { 16 | keepPreviousData: true, 17 | refetchOnMount: false, 18 | refetchOnWindowFocus: false, 19 | } 20 | ); 21 | function handlePaginationChange(e, value) { 22 | setPage(value); 23 | router.push(`paginationSSR/?page=${value}`, undefined, { shallow: true }); 24 | } 25 | return ( 26 |
27 |

28 | Rick and Morty with React Query and Pagination - Server Side rendered 29 |

30 | 38 |
39 | {data?.results?.map((character) => ( 40 |
41 | {character.name} 48 |
49 |

Name: {character.name}

50 |

Lives in: {character.location.name}

51 |

Species: {character.species}

52 | Id: {character.id} 53 |
54 |
55 | ))} 56 |
57 | 65 |
66 | ); 67 | } 68 | 69 | export async function getServerSideProps(context) { 70 | let page = 1; 71 | if (context.query.page) { 72 | page = parseInt(context.query.page); 73 | } 74 | const queryClient = new QueryClient(); 75 | await queryClient.prefetchQuery( 76 | ["characters", page], 77 | async () => 78 | await fetch( 79 | `https://rickandmortyapi.com/api/character/?page=${page}` 80 | ).then((result) => result.json()) 81 | ); 82 | return { props: { dehydratedState: dehydrate(queryClient) } }; 83 | } 84 | --------------------------------------------------------------------------------