{"resource":{"author":{"id":"h2so9H8jPMmgUKGghoNl","name":"Yomesh Gupta","username":"yomeshgupta"},"content":{"link":null,"difficulty":4,"domain":2,"type":1,"isInternal":true,"body":"In this blog post, we are going to see how can we add pagination to Remix using core web building blocks.\n\n## How does basic pagination work?\n\nThe idea behind pagination is that we only fetch the items of the current page where the value of the current page is tracked via the URL. For example --\n\n- [https://devtools.tech/questions/all/?page=1](https://devtools.tech/questions/all/?page=1)\n- [https://devtools.tech/questions/all/?page=2](https://devtools.tech/questions/all/?page=2)\n- [https://devtools.tech/questions/all/?page=3](https://devtools.tech/questions/all/?page=3)\n- [https://devtools.tech/questions/all/?page=4](https://devtools.tech/questions/all/?page=4)\n\nHere each page is rendered using SSR and showcases a particular section of the questions. This helps in creating shareable URLs that our end users can further use to share among themselves.\n\n## Data Fetching\n\nTo implement, we first need to understand how data fetching works in Remix.\n\n### Loader\n\nIn most simple terms, each route can define a `loader` function that is invoked on the server before rendering to provide data to the route. It handles all the `GET` requests for that route. If you are coming from the Next.js world then it is similar to `getServerSideProps`.\n\n```js\n// Route: /questions/all | File: questions.all.js\nimport { useLoaderData } from \"remix\";\nimport { getQuestions } from \"./data/questions.server.js\";\n\nexport const loader = async () => {\n  // Data fetching can happen here\n  // getQuestions talks to our API and returns an array of questions\n  const questions = await getQuestions();\n\n  return {\n    questions,\n  };\n};\n\nexport const QuestionsRoute = () => {\n  const { questions } = useLoaderData();\n\n  // can use questions here\n};\n```\n\nIn the above code snippet, we export the `loader` function where we make a call to our API to fetch all questions. We can now access the response from the loader in the component using a custom hook provided by Remix called `useLoaderData`.\n\n## Rendering Logic\n\nWe have data from the `loader` now we can render the list of questions.\n\n```js\nexport const QuestionsRoute = () => {\n  const { questions } = useLoaderData();\n\n  if (!questions || !questions.length) {\n    return <div>Empty...</div>;\n  }\n\n  return (\n    <ul>\n      {questions.map((question) => (\n        <li key={question.id}>{question.title}</li>\n      ))}\n    </ul>\n  );\n};\n```\n\n## Handling Navigation\n\nLet us assume that our loader data provides the value of the `currentPage`. We can use this data to show the `previous` and `next` buttons. There could be multiple ways to do this.\n\n1. Simple Link component\n\n```js\nimport { Link } from 'remix';\n\nexport const QuestionsRoute = () => {\n  const { questions, currentPage } = useLoaderData();\n  const pagination = useMemo(() => {\n    const previousPage = currentPage - 1 || 1;\n    const nextPage = currentPage + 1;\n\n    const pagination = {\n      previous: {\n        disabled: previousPage <= 1,\n        link: `/questions/all/?page=$${previousPage}`\n      },\n      next: {\n        disabled: questions && !questions.length, // or some empty state indicator\n        link: `/questions/all/?page=$${nextPage}`\n      }\n    }\n\n    return pagination;\n  }, [currentPage]);\n\n  ...\n\n  return (\n    <>\n      ...\n      <div className=\"navigation\">\n        <Link to={pagination.previous.link} className={pagination.previous.disabled ? 'disabled': ''}>\n          Previous\n        </Link>\n        <Link to={pagination.next.link} className={pagination.next.disabled ? 'disabled': ''}>\n          Next\n        </Link>\n      </div>\n    </>\n  );\n};\n```\n\nIn the above code snippet, we compute the UI state and link for `previous` and `next` using the `currentPage` value.\n\n- If the current page is the first page then `previous` is `disabled`.\n- If we have the `questions` array but it is empty then `next` is `disabled`.\n\n2. Navigating Programmatically\n\n```js\nimport { useNavigate } from 'remix';\n\nexport const QuestionsRoute = () => {\n  const { questions, currentPage } = useLoaderData();\n  const navigate = useNavigate();\n\n  const handleNavigation = (e) => {\n    const { target } = e;\n    const isForwardRequest = target.getAttribute('data-nav-operation') === 'next';\n    const offset = isForwardRequest ? 1 : -1;\n\n    navigate(`/questions/all/?page=${currentPage + offset}`)\n  };\n\n  ...\n\n  return (\n    <>\n      ...\n      <div className=\"navigation\">\n        <button onClick={handleNavigation} disabled={!currentPage} data-nav-operation=\"previous\">\n          Previous\n        </button>\n        <button onClick={handleNavigation} disabled={questions && !questions.length} data-nav-operation=\"next\">\n          Next\n        </button>\n      </div>\n    </>\n  );\n};\n```\n\nIn the above code snippet, we handle the navigation programmatically i.e. rather than rendering `Links`, we render `buttons` and on clicking of the buttons, we compute the next URL.\n\n2. Reading SearchParams Client Side\n\n```js\nimport { useSearchParams } from 'remix';\n\nexport const QuestionsRoute = () => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const currentPage = parseInt(searchParams.get(\"page\") || 0, 10);\n\n  const handleClick = (e) => {\n    const { target } = e;\n    const operation = target.getAttribute(\"data-nav-operation\");\n    const offset = operation === \"next\" ? 1 : -1;\n\n    setSearchParams({ page: currentPage + offset });\n  };\n\n  ...\n\n  return (\n    <>\n      ...\n      <div className=\"navigation\">\n        <button onClick={handleNavigation} disabled={!currentPage} data-nav-operation=\"previous\">\n          Previous\n        </button>\n        <button onClick={handleNavigation} disabled={questions && !questions.length} data-nav-operation=\"next\">\n          Next\n        </button>\n      </div>\n    </>\n  );\n};\n```\n\nIn this approach, it doesn't matter if we get the `currentPage` from the server. We read the value directly from the URL and update it based on user action. Due to URL updates, the remix will re-render the components and re-run the loaders.\n\nYou can use any approach that you feel is more apt for you!\n\n## Handling Query Params\n\nThe `loader` function gets multiple parameters like `params`, `context`, and `request`. We are most interested in the `request` parameter. It is a [Fetch Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) instance with information about the current request. We are going to use the `request` parameter to access the `page` query param and modify the data fetching logic.\n\n```js\nexport const loader = async ({ request }) => {\n  // Current url: devtools.tech/questions/all?page=2\n  const url = new URL(request.url);\n  const page = url.searchParams.get(\"page\") || 1;\n  const questions = await getQuestions({\n    page,\n  });\n\n  return {\n    questions,\n    currentPage: page,\n  };\n};\n```\n\nIn the above code snippet, we create a [new URL()](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) object bypassing the current request URL. We access the current page from `searchParams` and pass that to our `getQuestions` function. Now, our Backend API reads this parameter and returns the data for that particular page.\n\n## Everything in Action\n\n<video width=\"100%\" height=\"100%\" controls>\n  <source src=\"https://ik.imagekit.io/devtoolstech/add-pagination-to-remix-run/navigation-1_ohQ5T3-36t7.mp4?ik-sdk-version=javascript-1.4.3&updatedAt=1643448454820\" type=\"video/mp4\">\n</video>\n","languages":[],"editorConfig":{}},"stats":{"views":19626,"used":0,"likes":0},"description":"","published":true,"isActive":true,"tags":["javascript","pagination","remix run","frontend","tutorial","query params","navigation","js framework"],"slug":"add-pagination-to-remix-run---rid---A9jJkhqXgojvhovKZACe","isPremium":false,"categories":[],"requires":[],"_id":"61f505741238c849f9e159e5","title":"Add Pagination to Remix Run","resourceId":"A9jJkhqXgojvhovKZACe","createdAt":1643447668464,"modifiedAt":1643449786771},"currentUser":null,"isOwner":false,"recommendations":{"questions":[{"_id":"64e5c934d5ab2876a4c2a515","content":{"languages":["javascript","typescript"],"difficulty":2},"tags":["","javascript","frontend","youtube","camel case","problem solving","iteration","recursion","devtools tech","frontend interview questions","array","objects"],"slug":"implement-a-function-to-convert-all-object-keys-to-camel-case-or-javascript-interview-question---qid---9E4Ju40HMV36ksRPq9oF","title":"Implement a function to convert all object keys to camel case | JavaScript Interview Question","questionId":"9E4Ju40HMV36ksRPq9oF"},{"_id":"5ecb78d1be633f3afec7c691","content":{"languages":["javascript"],"difficulty":2},"tags":["node.js","javascript","promises","frontend","frontend fundamentals","js fundamentals","interview questions"],"slug":"what-would-be-the-output-of-the-following-code-snippet-or-promise-based-output-question-or-part-three---qid---RdUXrtoGZERd3KbB1tLh","title":"What would be the output of the following code snippet? | Promise Based Output Question | Part Three","questionId":"RdUXrtoGZERd3KbB1tLh"},{"_id":"63e1fb8822480f26b3cee189","content":{"languages":["react","html"],"difficulty":2},"tags":["javascript","react","youtube","frontend","coding challenges","ui","ux","devtools tech","react coding challenges","machine coding round","atlassion","google","cars24","devkode","codedamn","egghead","frontend masters","video","demo"],"slug":"how-to-build-a-file-explorer-atlassian-frontend-machine-coding-round-question-or-javascript-interview-question-or-react-js---qid---i2WQCZkIdpwGp2tG1LXJ","title":"How to build a File Explorer? Atlassian Frontend Machine Coding Round Question | JavaScript Interview Question | React.js","questionId":"i2WQCZkIdpwGp2tG1LXJ"},{"_id":"6689145240795030470d532c","content":{"languages":["react"],"difficulty":4},"tags":["avatar","coding","ui challenge","frontend","devtools tech","take home assignment","frontend ui challenge","tutorial","question","gmail"],"slug":"how-to-build-an-avatar-picker-frontend-ui-coding-challenge---qid---HuqxD3sw8pTmDfz3NvCi","title":"How to build an Avatar Picker? Frontend UI Coding Challenge","questionId":"HuqxD3sw8pTmDfz3NvCi"},{"_id":"5f1085eb3cce8c36cd292544","content":{"difficulty":1,"languages":"javascript"},"tags":["Javascript"],"slug":"switch-statement-output---qid---lMEhDVe38GcDli9sHfZZ","title":"Switch Statement Output","questionId":"lMEhDVe38GcDli9sHfZZ"}],"resources":[{"_id":"698de48a3354c04596030105","content":{"difficulty":4,"domain":1,"type":1,"isInternal":true,"languages":[]},"tags":["javascript","frontend","ui","ux","faang","interview experiences","coding"],"slug":"why-people-fail-at-interviews---rid---bQAGNih8yTqlrgq1O04x","title":"Why people fail at interviews?","resourceId":"bQAGNih8yTqlrgq1O04x"},{"_id":"632544a5ff34d52bbb0796d5","content":{"difficulty":1,"domain":2,"type":2,"isInternal":false},"tags":["javascript","frontend","js","advanced javascript","localstorage","browser","web storage","cookies","session storage","ui","tutorial","youtube"],"slug":"how-to-design-and-implement-localstorage-api-or-frontend-data-structures-or-javascript-interview-questions---rid---kh86UNH6qMxBEaWBcTgr","title":"How to design and implement LocalStorage API | Frontend Data Structures | JavaScript Interview Questions","resourceId":"kh86UNH6qMxBEaWBcTgr"},{"_id":"5f202d78cbec5f7ffc0c2fbd","content":{"difficulty":1,"domain":2,"type":2},"tags":["Lazy Loading"," Images"," React"," Intersection Observer"],"slug":"implementation-of-lazy-loading-image-oror-react-oror-intersection-observer---rid---iIx8G2ROXtX6ESYTpJ9a","title":"Implementation of Lazy Loading Image || React || Intersection Observer","resourceId":"iIx8G2ROXtX6ESYTpJ9a"},{"_id":"655600ac3e4c0459a44b430d","content":{"difficulty":4,"domain":2,"type":1,"isInternal":false,"languages":["javascript"]},"tags":["frontend","coding","ui","ux","razorpay","interview experience","tooling","devtools tech","hasnode","blog","tutorial"],"slug":"razorpay-frontend-engineer-interview-experience---rid---1JDZUAHqg0r2uYXxVPZW","title":"Razorpay Frontend Engineer interview experience","resourceId":"1JDZUAHqg0r2uYXxVPZW"},{"_id":"66a4ca5daac82a58634c2bfe","content":{"difficulty":4,"domain":2,"type":1,"isInternal":true,"languages":["undefined"]},"tags":["javascript","frontend","ui","ux","coding","atlassian","frontend coding challenge","feature flags","interactive shape"],"slug":"top-atlassian-frontend-interview-questions-and-process---rid---Bu7bJx9jNVoIsrH71Byb","title":"Top Atlassian Frontend Interview Questions and Process","resourceId":"Bu7bJx9jNVoIsrH71Byb"}]}}