From d8d2b87cb8452d39b053a0d279688a1af8a7d48d Mon Sep 17 00:00:00 2001 From: Troye Date: Sat, 2 Sep 2023 20:56:39 +0800 Subject: [PATCH] fix(docs): table async pagination (#1491) * fix(docs): change to swr * chore(table): change to swr * fix(table): keep the pagination when switching pages * chore(repo): pnpm lockfile updated --- .../examples/table/async-pagination/page.tsx | 61 +++++------------- .../app/examples/table/custom-styles/page.tsx | 2 +- .../docs/app/examples/table/use-case/page.tsx | 2 +- .../components/code-window/code-block.tsx | 4 +- .../components/table/async-pagination.ts | 62 +++++-------------- apps/docs/content/docs/components/table.mdx | 3 +- apps/docs/package.json | 13 ++-- .../components/card/stories/card.stories.tsx | 2 +- packages/components/table/package.json | 13 ++-- .../table/stories/table.stories.tsx | 61 +++++++----------- packages/core/theme/src/types.ts | 2 +- pnpm-lock.yaml | 16 ++++- 12 files changed, 93 insertions(+), 148 deletions(-) diff --git a/apps/docs/app/examples/table/async-pagination/page.tsx b/apps/docs/app/examples/table/async-pagination/page.tsx index e29ddf765..10f85cddb 100644 --- a/apps/docs/app/examples/table/async-pagination/page.tsx +++ b/apps/docs/app/examples/table/async-pagination/page.tsx @@ -11,8 +11,8 @@ import { Spinner, Pagination, } from "@nextui-org/react"; -import {useAsyncList} from "@react-stately/data"; -import {useCallback, useMemo, useState} from "react"; +import {useMemo, useState} from "react"; +import useSWR from "swr"; type SWCharacter = { name: string; @@ -21,50 +21,23 @@ type SWCharacter = { birth_year: string; }; +const fetcher = (...args: Parameters) => fetch(...args).then((res) => res.json()); + export default function Page() { const [page, setPage] = useState(1); - const [total, setTotal] = useState(0); - const [isLoading, setIsLoading] = useState(true); + + const {data, isLoading} = useSWR<{ + count: number; + results: SWCharacter[]; + }>(`https://swapi.py4e.com/api/people?page=${page}`, fetcher, { + keepPreviousData: true, + }); const rowsPerPage = 10; - let list = useAsyncList({ - async load({signal, cursor}) { - // If no cursor is available, then we're loading the first page. - // Otherwise, the cursor is the next URL to load, as returned from the previous page. - const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal}); - let json = await res.json(); - - setTotal(json.count); - - setIsLoading(false); - - return { - items: json.results, - cursor: json.next, - }; - }, - }); - - const pages = Math.ceil(total / rowsPerPage); - - const items = useMemo(() => { - const start = (page - 1) * rowsPerPage; - const end = start + rowsPerPage; - - return list.items.slice(start, end); - }, [page, list.items?.length]); - - const onPaginationChange = useCallback( - (page: number) => { - setIsLoading(true); - if (page >= list.items.length / rowsPerPage) { - list.loadMore(); - } - setPage(page); - }, - [list.items.length], - ); + const pages = useMemo(() => { + return data?.count ? Math.ceil(data.count / rowsPerPage) : 0; + }, [data?.count, rowsPerPage]); return (
@@ -80,7 +53,7 @@ export default function Page() { color="primary" page={page} total={pages} - onChange={onPaginationChange} + onChange={(page) => setPage(page)} />
) : null @@ -96,8 +69,8 @@ export default function Page() { Birth year } > {(item) => ( diff --git a/apps/docs/app/examples/table/custom-styles/page.tsx b/apps/docs/app/examples/table/custom-styles/page.tsx index 14819940b..dd5cbe7ad 100644 --- a/apps/docs/app/examples/table/custom-styles/page.tsx +++ b/apps/docs/app/examples/table/custom-styles/page.tsx @@ -256,7 +256,7 @@ const users = [ }, ]; -type User = (typeof users)[0]; +type User = typeof users[0]; export default function Page() { const [filterValue, setFilterValue] = useState(""); diff --git a/apps/docs/app/examples/table/use-case/page.tsx b/apps/docs/app/examples/table/use-case/page.tsx index 5182871b8..b223445b0 100644 --- a/apps/docs/app/examples/table/use-case/page.tsx +++ b/apps/docs/app/examples/table/use-case/page.tsx @@ -256,7 +256,7 @@ const users = [ }, ]; -type User = (typeof users)[0]; +type User = typeof users[0]; export default function Page() { const [filterValue, setFilterValue] = useState(""); diff --git a/apps/docs/components/code-window/code-block.tsx b/apps/docs/components/code-window/code-block.tsx index 74de828f2..385ff7a9a 100644 --- a/apps/docs/components/code-window/code-block.tsx +++ b/apps/docs/components/code-window/code-block.tsx @@ -108,9 +108,9 @@ function CodeTypewriter({value, className, css, ...props}: any) { return (
       
     
@@ -155,7 +155,7 @@ const CodeBlock = React.forwardRef((_props, forw {...props} > {showWindowIcons && } - + ); }); diff --git a/apps/docs/content/components/table/async-pagination.ts b/apps/docs/content/components/table/async-pagination.ts index 9f44e73b8..54106b07e 100644 --- a/apps/docs/content/components/table/async-pagination.ts +++ b/apps/docs/content/components/table/async-pagination.ts @@ -1,50 +1,22 @@ const App = `import {Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Pagination, Spinner, getKeyValue} from "@nextui-org/react"; -import {useAsyncList} from "@react-stately/data"; +import useSWR from "swr"; + +const fetcher = (...args) => fetch(...args).then((res) => res.json()); export default function App() { const [page, setPage] = React.useState(1); - const [total, setTotal] = React.useState(0); - const [isLoading, setIsLoading] = React.useState(true); + + const {data, isLoading} = useSWR(\`https://swapi.py4e.com/api/people?page=\$\{page\}\`, fetcher, { + keepPreviousData: true, + }); const rowsPerPage = 10; - let list = useAsyncList({ - async load({signal, cursor}) { - // If no cursor is available, then we're loading the first page. - // Otherwise, the cursor is the next URL to load, as returned from the previous page. - const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal}); - let json = await res.json(); + const pages = useMemo(() => { + return data?.count ? Math.ceil(data.count / rowsPerPage) : 0; + }, [data?.count, rowsPerPage]); - setTotal(json.count); - - setIsLoading(false); - - return { - items: json.results, - cursor: json.next, - }; - }, - }); - - const pages = Math.ceil(total / rowsPerPage); - - const items = React.useMemo(() => { - const start = (page - 1) * rowsPerPage; - const end = start + rowsPerPage; - - return list.items.slice(start, end); - }, [page, list.items?.length]); - - const onPaginationChange = React.useCallback( - (page) => { - setIsLoading(true); - if (page >= list.items.length / rowsPerPage) { - list.loadMore(); - } - setPage(page); - }, - [list.items.length], - ); + const loadingState = isLoading || data?.results.length === 0 ? "loading" : "idle"; return ( setPage(page)} /> ) : null } - classNames={{ - table: "min-h-[400px]", - }} + {...args} > Name @@ -75,12 +45,12 @@ export default function App() { Birth year } + loadingState={loadingState} > {(item) => ( - + {(columnKey) => {getKeyValue(item, columnKey)}} )} diff --git a/apps/docs/content/docs/components/table.mdx b/apps/docs/content/docs/components/table.mdx index 0603991b0..573b9fd35 100644 --- a/apps/docs/content/docs/components/table.mdx +++ b/apps/docs/content/docs/components/table.mdx @@ -257,8 +257,7 @@ You can use the [Pagination](/components/pagination) component to paginate the t ### Async Pagination -It is also possible to use the [Pagination](/components/pagination) component to paginate the table asynchronously. To fetch the data, we are using the `useAsyncList` hook from [@react-stately/data](https://react-spectrum.adobe.com/react-stately/useAsyncList.html). -Please check the installation instructions in the [Sorting Rows](#sorting-rows) section. +It is also possible to use the [Pagination](/components/pagination) component to paginate the table asynchronously. To fetch the data, we are using the `useSWR` hook from [SWR](https://swr.vercel.app/docs/pagination). { }, ]; - type ListItem = (typeof list)[number]; + type ListItem = typeof list[number]; const handlePress = (item: ListItem) => { // eslint-disable-next-line no-console diff --git a/packages/components/table/package.json b/packages/components/table/package.json index 3d3f31db8..dd7e65975 100644 --- a/packages/components/table/package.json +++ b/packages/components/table/package.json @@ -38,9 +38,9 @@ }, "dependencies": { "@nextui-org/checkbox": "workspace:*", - "@nextui-org/shared-utils": "workspace:*", "@nextui-org/react-utils": "workspace:*", "@nextui-org/shared-icons": "workspace:*", + "@nextui-org/shared-utils": "workspace:*", "@nextui-org/spacer": "workspace:*", "@nextui-org/system": "workspace:*", "@nextui-org/theme": "workspace:*", @@ -49,22 +49,23 @@ "@react-aria/table": "^3.11.0", "@react-aria/utils": "^3.19.0", "@react-aria/visually-hidden": "^3.8.3", - "@react-stately/virtualizer": "^3.6.0", "@react-stately/table": "^3.11.0", + "@react-stately/virtualizer": "^3.6.0", "@react-types/grid": "^3.2.0", "@react-types/table": "^3.8.0" }, "devDependencies": { - "@nextui-org/chip": "workspace:*", "@nextui-org/button": "workspace:*", - "@nextui-org/spinner": "workspace:*", + "@nextui-org/chip": "workspace:*", "@nextui-org/pagination": "workspace:*", - "@nextui-org/use-infinite-scroll": "workspace:*", + "@nextui-org/spinner": "workspace:*", "@nextui-org/tooltip": "workspace:*", + "@nextui-org/use-infinite-scroll": "workspace:*", "@nextui-org/user": "workspace:*", "@react-stately/data": "^3.10.1", "clean-package": "2.2.0", - "react": "^18.0.0" + "react": "^18.0.0", + "swr": "^2.2.1" }, "clean-package": "../../../clean-package.config.json" } diff --git a/packages/components/table/stories/table.stories.tsx b/packages/components/table/stories/table.stories.tsx index b3fc4b7d5..717053b90 100644 --- a/packages/components/table/stories/table.stories.tsx +++ b/packages/components/table/stories/table.stories.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useMemo} from "react"; import {Meta} from "@storybook/react"; import {table} from "@nextui-org/theme"; import {User} from "@nextui-org/user"; @@ -10,6 +10,7 @@ import {Tooltip} from "@nextui-org/tooltip"; import {EditIcon, DeleteIcon, EyeIcon} from "@nextui-org/shared-icons"; import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll"; import {useAsyncList} from "@react-stately/data"; +import useSWR from "swr"; import { Table, @@ -236,7 +237,7 @@ const CustomCellTemplate = (args: TableProps) => { }, ]; - type User = (typeof users)[0]; + type User = typeof users[0]; const statusColorMap: Record = { active: "success", @@ -376,7 +377,7 @@ const CustomCellWithClassnamesTemplate = (args: TableProps) => { }, ]; - type User = (typeof users)[0]; + type User = typeof users[0]; const statusColorMap: Record = { active: "success", @@ -758,38 +759,25 @@ const PaginatedTemplate = (args: TableProps) => { ); }; +const fetcher = (...args: Parameters) => fetch(...args).then((res) => res.json()); + const AsyncPaginatedTemplate = (args: TableProps) => { const [page, setPage] = React.useState(1); - const [total, setTotal] = React.useState(0); + + const {data, isLoading} = useSWR<{ + count: number; + results: SWCharacter[]; + }>(`https://swapi.py4e.com/api/people?page=${page}`, fetcher, { + keepPreviousData: true, + }); const rowsPerPage = 10; - let list = useAsyncList({ - async load({signal, cursor}) { - // If no cursor is available, then we're loading the first page. - // Otherwise, the cursor is the next URL to load, as returned from the previous page. - const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal}); - let json = await res.json(); + const pages = useMemo(() => { + return data?.count ? Math.ceil(data?.count / rowsPerPage) : 0; + }, [data?.count, rowsPerPage]); - setTotal(json.count); - - return { - items: json.results, - cursor: json.next, - }; - }, - }); - - const pages = Math.ceil(total / rowsPerPage); - - const items = React.useMemo(() => { - const start = (page - 1) * rowsPerPage; - const end = start + rowsPerPage; - - return list.items.slice(start, end); - }, [page, list.items?.length, list.loadingState]); - - const loadingState = items.length === 0 ? "loading" : list.loadingState; + const loadingState = isLoading || data?.results.length === 0 ? "loading" : "idle"; return (
{ color="primary" page={page} total={pages} - onChange={(page) => { - if (page >= list.items.length / rowsPerPage) { - list.loadMore(); - } - setPage(page); - }} + onChange={(page) => setPage(page)} /> ) : null @@ -822,9 +805,13 @@ const AsyncPaginatedTemplate = (args: TableProps) => { MassBirth year - } loadingState={loadingState}> + } + loadingState={loadingState} + > {(item) => ( - + {(columnKey) => {getKeyValue(item, columnKey)}} )} diff --git a/packages/core/theme/src/types.ts b/packages/core/theme/src/types.ts index 14a3c040e..c8f826259 100644 --- a/packages/core/theme/src/types.ts +++ b/packages/core/theme/src/types.ts @@ -65,7 +65,7 @@ export const spacingScaleKeys = [ export const mappedSpacingScaleKeys = spacingScaleKeys.map((key) => `unit-${key}`); -export type SpacingScaleKeys = (typeof spacingScaleKeys)[number]; +export type SpacingScaleKeys = typeof spacingScaleKeys[number]; export type SpacingScale = Partial>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0dd4d253..0b61aa845 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,6 +471,9 @@ importers: shelljs: specifier: ^0.8.4 version: 0.8.5 + swr: + specifier: ^2.2.1 + version: 2.2.1(react@18.2.0) tailwind-variants: specifier: ^0.1.13 version: 0.1.13(tailwindcss@3.3.3) @@ -2009,6 +2012,9 @@ importers: react: specifier: ^18.2.0 version: 18.2.0 + swr: + specifier: ^2.2.1 + version: 2.2.1(react@18.2.0) packages/components/tabs: dependencies: @@ -22956,6 +22962,15 @@ packages: stable: 0.1.8 dev: true + /swr@2.2.1(react@18.2.0): + resolution: {integrity: sha512-KJVA7dGtOBeZ+2sycEuzUfVIP5lZ/cd0xjevv85n2YG0x1uHJQicjAtahVZL6xG3+TjqhbBqimwYzVo3saeVXQ==} + peerDependencies: + react: ^18.2.0 + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true @@ -24036,7 +24051,6 @@ packages: react: ^18.2.0 dependencies: react: 18.2.0 - dev: false /use@3.1.1: resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}