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
This commit is contained in:
Troye 2023-09-02 20:56:39 +08:00 committed by GitHub
parent 64571e468c
commit d8d2b87cb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 93 additions and 148 deletions

View File

@ -11,8 +11,8 @@ import {
Spinner, Spinner,
Pagination, Pagination,
} from "@nextui-org/react"; } from "@nextui-org/react";
import {useAsyncList} from "@react-stately/data"; import {useMemo, useState} from "react";
import {useCallback, useMemo, useState} from "react"; import useSWR from "swr";
type SWCharacter = { type SWCharacter = {
name: string; name: string;
@ -21,50 +21,23 @@ type SWCharacter = {
birth_year: string; birth_year: string;
}; };
const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then((res) => res.json());
export default function Page() { export default function Page() {
const [page, setPage] = useState(1); 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; const rowsPerPage = 10;
let list = useAsyncList<SWCharacter>({ const pages = useMemo(() => {
async load({signal, cursor}) { return data?.count ? Math.ceil(data.count / rowsPerPage) : 0;
// If no cursor is available, then we're loading the first page. }, [data?.count, rowsPerPage]);
// 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],
);
return ( return (
<div className="p-6"> <div className="p-6">
@ -80,7 +53,7 @@ export default function Page() {
color="primary" color="primary"
page={page} page={page}
total={pages} total={pages}
onChange={onPaginationChange} onChange={(page) => setPage(page)}
/> />
</div> </div>
) : null ) : null
@ -96,8 +69,8 @@ export default function Page() {
<TableColumn key="birth_year">Birth year</TableColumn> <TableColumn key="birth_year">Birth year</TableColumn>
</TableHeader> </TableHeader>
<TableBody <TableBody
isLoading={isLoading && !items.length} isLoading={isLoading || data?.results.length === 0}
items={items} items={data?.results ?? []}
loadingContent={<Spinner />} loadingContent={<Spinner />}
> >
{(item) => ( {(item) => (

View File

@ -256,7 +256,7 @@ const users = [
}, },
]; ];
type User = (typeof users)[0]; type User = typeof users[0];
export default function Page() { export default function Page() {
const [filterValue, setFilterValue] = useState(""); const [filterValue, setFilterValue] = useState("");

View File

@ -256,7 +256,7 @@ const users = [
}, },
]; ];
type User = (typeof users)[0]; type User = typeof users[0];
export default function Page() { export default function Page() {
const [filterValue, setFilterValue] = useState(""); const [filterValue, setFilterValue] = useState("");

View File

@ -108,9 +108,9 @@ function CodeTypewriter({value, className, css, ...props}: any) {
return ( return (
<Pre className={className} css={css} {...props}> <Pre className={className} css={css} {...props}>
<code <code
dangerouslySetInnerHTML={{__html: value}}
ref={wrapperRef} ref={wrapperRef}
className={className} className={className}
dangerouslySetInnerHTML={{__html: value}}
style={{opacity: 0}} style={{opacity: 0}}
/> />
</Pre> </Pre>
@ -155,7 +155,7 @@ const CodeBlock = React.forwardRef<HTMLPreElement, CodeBlockProps>((_props, forw
{...props} {...props}
> >
{showWindowIcons && <WindowActions title={title} />} {showWindowIcons && <WindowActions title={title} />}
<code dangerouslySetInnerHTML={{__html: result}} className={clsx(classes, codeClasses)} /> <code className={clsx(classes, codeClasses)} dangerouslySetInnerHTML={{__html: result}} />
</Pre> </Pre>
); );
}); });

View File

@ -1,50 +1,22 @@
const App = `import {Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Pagination, Spinner, getKeyValue} from "@nextui-org/react"; 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() { export default function App() {
const [page, setPage] = React.useState(1); 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; const rowsPerPage = 10;
let list = useAsyncList({ const pages = useMemo(() => {
async load({signal, cursor}) { return data?.count ? Math.ceil(data.count / rowsPerPage) : 0;
// If no cursor is available, then we're loading the first page. }, [data?.count, rowsPerPage]);
// 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); const loadingState = isLoading || data?.results.length === 0 ? "loading" : "idle";
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],
);
return ( return (
<Table <Table
@ -59,14 +31,12 @@ export default function App() {
color="primary" color="primary"
page={page} page={page}
total={pages} total={pages}
onChange={onPaginationChange} onChange={(page) => setPage(page)}
/> />
</div> </div>
) : null ) : null
} }
classNames={{ {...args}
table: "min-h-[400px]",
}}
> >
<TableHeader> <TableHeader>
<TableColumn key="name">Name</TableColumn> <TableColumn key="name">Name</TableColumn>
@ -75,12 +45,12 @@ export default function App() {
<TableColumn key="birth_year">Birth year</TableColumn> <TableColumn key="birth_year">Birth year</TableColumn>
</TableHeader> </TableHeader>
<TableBody <TableBody
isLoading={isLoading && !items.length} items={data?.results ?? []}
items={items}
loadingContent={<Spinner />} loadingContent={<Spinner />}
loadingState={loadingState}
> >
{(item) => ( {(item) => (
<TableRow key={item.name}> <TableRow key={item?.name}>
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>} {(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
</TableRow> </TableRow>
)} )}

View File

@ -257,8 +257,7 @@ You can use the [Pagination](/components/pagination) component to paginate the t
### Async Pagination ### 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). 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).
Please check the installation instructions in the [Sorting Rows](#sorting-rows) section.
<CodeDemo <CodeDemo
asIframe asIframe

View File

@ -18,17 +18,17 @@
"@codesandbox/sandpack-react": "^2.6.4", "@codesandbox/sandpack-react": "^2.6.4",
"@mapbox/rehype-prism": "^0.6.0", "@mapbox/rehype-prism": "^0.6.0",
"@nextui-org/aria-utils": "workspace:*", "@nextui-org/aria-utils": "workspace:*",
"@nextui-org/badge": "workspace:*",
"@nextui-org/code": "workspace:*",
"@nextui-org/divider": "workspace:*",
"@nextui-org/kbd": "workspace:*",
"@nextui-org/react": "workspace:*", "@nextui-org/react": "workspace:*",
"@nextui-org/shared-icons": "workspace:*", "@nextui-org/shared-icons": "workspace:*",
"@nextui-org/shared-utils": "workspace:*", "@nextui-org/shared-utils": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/spacer": "workspace:*",
"@nextui-org/kbd": "workspace:*",
"@nextui-org/code": "workspace:*",
"@nextui-org/badge": "workspace:*",
"@nextui-org/skeleton": "workspace:*", "@nextui-org/skeleton": "workspace:*",
"@nextui-org/spacer": "workspace:*",
"@nextui-org/spinner": "workspace:*", "@nextui-org/spinner": "workspace:*",
"@nextui-org/divider": "workspace:*", "@nextui-org/theme": "workspace:*",
"@nextui-org/use-clipboard": "workspace:*", "@nextui-org/use-clipboard": "workspace:*",
"@nextui-org/use-infinite-scroll": "workspace:*", "@nextui-org/use-infinite-scroll": "workspace:*",
"@nextui-org/use-is-mobile": "workspace:*", "@nextui-org/use-is-mobile": "workspace:*",
@ -84,6 +84,7 @@
"scroll-into-view-if-needed": "3.0.10", "scroll-into-view-if-needed": "3.0.10",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"shelljs": "^0.8.4", "shelljs": "^0.8.4",
"swr": "^2.2.1",
"tailwind-variants": "^0.1.13", "tailwind-variants": "^0.1.13",
"unified": "^9.2.2", "unified": "^9.2.2",
"unist-util-visit": "^4.1.2", "unist-util-visit": "^4.1.2",

View File

@ -319,7 +319,7 @@ const PrimaryActionTemplate = (args: CardProps) => {
}, },
]; ];
type ListItem = (typeof list)[number]; type ListItem = typeof list[number];
const handlePress = (item: ListItem) => { const handlePress = (item: ListItem) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -38,9 +38,9 @@
}, },
"dependencies": { "dependencies": {
"@nextui-org/checkbox": "workspace:*", "@nextui-org/checkbox": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/react-utils": "workspace:*", "@nextui-org/react-utils": "workspace:*",
"@nextui-org/shared-icons": "workspace:*", "@nextui-org/shared-icons": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/spacer": "workspace:*", "@nextui-org/spacer": "workspace:*",
"@nextui-org/system": "workspace:*", "@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*", "@nextui-org/theme": "workspace:*",
@ -49,22 +49,23 @@
"@react-aria/table": "^3.11.0", "@react-aria/table": "^3.11.0",
"@react-aria/utils": "^3.19.0", "@react-aria/utils": "^3.19.0",
"@react-aria/visually-hidden": "^3.8.3", "@react-aria/visually-hidden": "^3.8.3",
"@react-stately/virtualizer": "^3.6.0",
"@react-stately/table": "^3.11.0", "@react-stately/table": "^3.11.0",
"@react-stately/virtualizer": "^3.6.0",
"@react-types/grid": "^3.2.0", "@react-types/grid": "^3.2.0",
"@react-types/table": "^3.8.0" "@react-types/table": "^3.8.0"
}, },
"devDependencies": { "devDependencies": {
"@nextui-org/chip": "workspace:*",
"@nextui-org/button": "workspace:*", "@nextui-org/button": "workspace:*",
"@nextui-org/spinner": "workspace:*", "@nextui-org/chip": "workspace:*",
"@nextui-org/pagination": "workspace:*", "@nextui-org/pagination": "workspace:*",
"@nextui-org/use-infinite-scroll": "workspace:*", "@nextui-org/spinner": "workspace:*",
"@nextui-org/tooltip": "workspace:*", "@nextui-org/tooltip": "workspace:*",
"@nextui-org/use-infinite-scroll": "workspace:*",
"@nextui-org/user": "workspace:*", "@nextui-org/user": "workspace:*",
"@react-stately/data": "^3.10.1", "@react-stately/data": "^3.10.1",
"clean-package": "2.2.0", "clean-package": "2.2.0",
"react": "^18.0.0" "react": "^18.0.0",
"swr": "^2.2.1"
}, },
"clean-package": "../../../clean-package.config.json" "clean-package": "../../../clean-package.config.json"
} }

View File

@ -1,4 +1,4 @@
import React from "react"; import React, {useMemo} from "react";
import {Meta} from "@storybook/react"; import {Meta} from "@storybook/react";
import {table} from "@nextui-org/theme"; import {table} from "@nextui-org/theme";
import {User} from "@nextui-org/user"; 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 {EditIcon, DeleteIcon, EyeIcon} from "@nextui-org/shared-icons";
import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll"; import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";
import {useAsyncList} from "@react-stately/data"; import {useAsyncList} from "@react-stately/data";
import useSWR from "swr";
import { import {
Table, Table,
@ -236,7 +237,7 @@ const CustomCellTemplate = (args: TableProps) => {
}, },
]; ];
type User = (typeof users)[0]; type User = typeof users[0];
const statusColorMap: Record<string, ChipProps["color"]> = { const statusColorMap: Record<string, ChipProps["color"]> = {
active: "success", active: "success",
@ -376,7 +377,7 @@ const CustomCellWithClassnamesTemplate = (args: TableProps) => {
}, },
]; ];
type User = (typeof users)[0]; type User = typeof users[0];
const statusColorMap: Record<string, ChipProps["color"]> = { const statusColorMap: Record<string, ChipProps["color"]> = {
active: "success", active: "success",
@ -758,38 +759,25 @@ const PaginatedTemplate = (args: TableProps) => {
); );
}; };
const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then((res) => res.json());
const AsyncPaginatedTemplate = (args: TableProps) => { const AsyncPaginatedTemplate = (args: TableProps) => {
const [page, setPage] = React.useState(1); 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; const rowsPerPage = 10;
let list = useAsyncList<SWCharacter>({ const pages = useMemo(() => {
async load({signal, cursor}) { return data?.count ? Math.ceil(data?.count / rowsPerPage) : 0;
// If no cursor is available, then we're loading the first page. }, [data?.count, rowsPerPage]);
// 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); const loadingState = isLoading || data?.results.length === 0 ? "loading" : "idle";
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;
return ( return (
<Table <Table
@ -804,12 +792,7 @@ const AsyncPaginatedTemplate = (args: TableProps) => {
color="primary" color="primary"
page={page} page={page}
total={pages} total={pages}
onChange={(page) => { onChange={(page) => setPage(page)}
if (page >= list.items.length / rowsPerPage) {
list.loadMore();
}
setPage(page);
}}
/> />
</div> </div>
) : null ) : null
@ -822,9 +805,13 @@ const AsyncPaginatedTemplate = (args: TableProps) => {
<TableColumn key="mass">Mass</TableColumn> <TableColumn key="mass">Mass</TableColumn>
<TableColumn key="birth_year">Birth year</TableColumn> <TableColumn key="birth_year">Birth year</TableColumn>
</TableHeader> </TableHeader>
<TableBody items={items} loadingContent={<Spinner />} loadingState={loadingState}> <TableBody
items={data?.results ?? []}
loadingContent={<Spinner />}
loadingState={loadingState}
>
{(item) => ( {(item) => (
<TableRow key={item.name}> <TableRow key={item?.name}>
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>} {(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
</TableRow> </TableRow>
)} )}

View File

@ -65,7 +65,7 @@ export const spacingScaleKeys = [
export const mappedSpacingScaleKeys = spacingScaleKeys.map((key) => `unit-${key}`); export const mappedSpacingScaleKeys = spacingScaleKeys.map((key) => `unit-${key}`);
export type SpacingScaleKeys = (typeof spacingScaleKeys)[number]; export type SpacingScaleKeys = typeof spacingScaleKeys[number];
export type SpacingScale = Partial<Record<SpacingScaleKeys, string>>; export type SpacingScale = Partial<Record<SpacingScaleKeys, string>>;

16
pnpm-lock.yaml generated
View File

@ -471,6 +471,9 @@ importers:
shelljs: shelljs:
specifier: ^0.8.4 specifier: ^0.8.4
version: 0.8.5 version: 0.8.5
swr:
specifier: ^2.2.1
version: 2.2.1(react@18.2.0)
tailwind-variants: tailwind-variants:
specifier: ^0.1.13 specifier: ^0.1.13
version: 0.1.13(tailwindcss@3.3.3) version: 0.1.13(tailwindcss@3.3.3)
@ -2009,6 +2012,9 @@ importers:
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
swr:
specifier: ^2.2.1
version: 2.2.1(react@18.2.0)
packages/components/tabs: packages/components/tabs:
dependencies: dependencies:
@ -22956,6 +22962,15 @@ packages:
stable: 0.1.8 stable: 0.1.8
dev: true 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: /symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: true dev: true
@ -24036,7 +24051,6 @@ packages:
react: ^18.2.0 react: ^18.2.0
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
dev: false
/use@3.1.1: /use@3.1.1:
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}