使用 Next.js、MUI 和 react-query 实现分页
如果您需要获取、缓存页面上的数据,并进行精美的分页,以获得出色的用户体验,那么您来对地方了。几天前,我在工作中实现了一个解决这个问题的解决方案,想与大家分享:
1. 设置项目
我不想用冗长的篇幅来烦你设置和创建样板,所以我假设你已经熟悉了基础知识。如果你还有疑问,也可以在这个仓库里查看已经完成的项目。开始吧:
- 您需要一个全新的 Next.js 项目,并安装 React Query 和 Material-ui。我选择了 Material-ui v4,因为我们工作中使用的就是这个版本,但您也可以使用其他版本,只需记住 import 语句和用法可能略有不同。
- 首先,你需要从 Rick and Morty API 获取一些需要分页的数据。我们不会使用 useEffect hook 来获取数据,而是使用 react-query。为了使其正常工作,你需要在 _app.js 文件中配置一个提供程序:
import "../styles/globals.css";
import { ReactQueryDevtools } from "react-query/devtools";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
function MyApp({ Component, pageProps }) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools>
</QueryClientProvider>
);
}
export default MyApp;
这是纯粹从react-query 文档中获取的设置:我们配置了一个不带选项的 queryClient,并将我们的应用程序包装在 QueryClientProvider 中。此外,我还添加了 ReactQueryDevtools,以便更轻松地查看数据以及缓存的工作原理。
2. 使用 react-query 获取并显示数据
- 现在,在 index.js 页面或您选择的任何其他页面中,导入 useQuery 钩子。它接受两个参数:第一个参数是一个字符串,用作查询的名称;第二个参数是用于获取数据的函数。为了能够在页面上看到一些内容,我将字符串化的数据打印在 div 标签内。
import { useQuery } from "react-query";
export default function PaginationPage(props) {
const { data } = useQuery(
"characters",
async () =>
await fetch(`https://rickandmortyapi.com/api/character/`).then((result) =>
result.json()
)
);
console.log(data);
return <div>{JSON.stringify(data)}</div>;
}
结果应该与上图类似。请记住,您仍在异步获取数据,因此正如您在控制台中看到的,在开始时会有一段时间数据对象未定义。此外,如果您点击左上角的花朵图标,则会打开 React-Query 开发者工具。在那里,您可以看到刚刚执行的查询,当您点击它时,它甚至会让您看到获取的查询数据,因此您实际上不需要我编写的 console.log。
- 现在我们的应用程序中已经有了一些数据,让我们快速设置一些看起来不错的东西来显示我们刚刚获取的 Rick 和 Morty 角色:
<h1>Rick and Morty with React Query and Pagination</h1>
<div className='grid-container'>
{data?.results?.map((character) => (
<article key={character.id}>
<img
src={character.image}
alt={character.name}
height={200}
loading='lazy'
width={200}
/>
<div className='text'>
<p>Name: {character.name}</p>
<p>Lives in: {character.location.name}</p>
<p>Species: {character.species}</p>
<i>Id: {character.id} </i>
</div>
</article>
))}
</div>
这里没什么特别的:如果有数据,我们就迭代它,然后显示一张图片和一些关于角色的数据。 样式如下,我只是把它们写在了 globals.css 文件中。虽然看起来不太酷,但确实能用。
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
max-width: 1300px;
width: 100%;
margin: auto;
padding: 2rem;
}
article {
padding: 1em;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
border-radius: 0.5em;
box-shadow: rgba(99, 99, 99, 0.5) 0px 2px 8px 0px;
}
到目前为止,我们的应用程序无法显示 API 默认返回的前 20 个项目之外的数据,所以让我们改变这一点。
3. 使用 Material UI 添加分页
- 导入 Material UI 分页组件并将其放置在网格容器上方。 count 属性控制显示的页面数量,我们已经从 API 中获取了此信息。
import Pagination from "@material-ui/lab/Pagination";
...
return (
<div>
<h1>Rick and Morty with React Query and Pagination</h1>
<Pagination
count={data?.info.pages}
variant='outlined'
color='primary'
className='pagination'
/>
<div className='grid-container'>
...
- 然后,设置一些状态来保存当前页面,并将页面参数添加到 API 调用中。这也意味着我们可以将当前页面传递给 MUI 分页组件,以便它知道要突出显示哪个数字。
import { useState } from "react";
...
const [page, setPage] = useState(1);
const { data } = useQuery(
"characters",
async () =>
await fetch(
`https://rickandmortyapi.com/api/character/?page=${page}`
).then((result) => result.json())
);
return (
<div>
<h1>Rick and Morty with React Query and Pagination</h1>
<Pagination
count={data?.info.pages}
variant='outlined'
color='primary'
className='pagination'
page={page}
/>
...
- 最后一步,我们需要为 Pagination 组件定义 onChange 处理程序。该处理程序会更新页面状态,并对 URL 进行浅层推送。为了让 react-query 获取新数据,我们必须将 page 变量添加到查询键。我们将传入一个数组,其中包含字符串以及所有我们想要触发新 API 调用的变量,而不是字符串“characters”。
import { useRouter } from "next/router";
...
const router = useRouter();
const { data } = useQuery(
["characters", page],
async () =>
await fetch(
`https://rickandmortyapi.com/api/character/?page=${page}`
).then((result) => result.json())
);
function handlePaginationChange(e, value) {
setPage(value);
router.push(`pagination/?page=${value}`, undefined, { shallow: true });
}
return (
<div>
<h1>Rick and Morty with React Query and Pagination</h1>
<Pagination
count={data?.info.pages}
variant='outlined'
color='primary'
className='pagination'
page={page}
onChange={handlePaginationChange}
/>
现在,分页功能已经完美运行!点击不同的页面,虽然你已经看过《瑞克和莫蒂》的所有季了,但你还是会被那些你不认识的角色弄得眼花缭乱……
4. 外观改进
这里有两个小问题无法正常工作:第一个问题是,当用户直接访问 URL 时my-domain.com/pagination?page=5
,我们的应用程序不会显示第 5 页的结果,因为我们从未在页面加载时读取查询参数。我们可以使用 useEffect hook 来解决这个问题,该 hook 从 Next.js 路由器对象读取 queryParam,并且仅在所有组件首次挂载时运行:
useEffect(() => {
if (router.query.page) {
setPage(parseInt(router.query.page));
}
}, [router.query.page]);
另一方面,当你点击页面时,你会看到 Pagination 组件闪烁:每次获取数据时,它都会获取页面长度信息,但由于数据未定义,获取过程中页面会缩小到只显示一页。我们可以通过在 useQuery 钩子上设置一个配置对象来避免这种情况:
const { data } = useQuery(
["characters", page],
async () =>
await fetch(
`https://rickandmortyapi.com/api/character/?page=${page}`
).then((result) => result.json()),
{
keepPreviousData: true,
}
);
keepPreviousData 指令将在获取数据对象时保留先前的数据,并在已经有新数据时替换它,从而避免数据暂时未定义的情况。
希望这些对你有帮助!如果你能成功,或者有什么反馈,请告诉我。
现在,恕我直言,我得去看《瑞克和莫蒂》了,因为剧里的角色都让我很想重温一遍。