Next.js论坛分页:setState与UI不同步问题求助
Next.js论坛分页状态不同步问题解决
问题详情
开发Next.js论坛项目时,分页功能出现状态不同步:初始通过getServerSideProps加载第一页数据,点击分页按钮调用API获取对应页数据,但setPage设置的页码与UI显示始终相差1——点击下一页时,API请求的是旧页码,UI却先更新为新页码,导致数据和页码不匹配。
用户代码如下:
import dayjs from 'dayjs' import type { GetServerSidePropsResult } from 'next' import Head from 'next/head' import Link from 'next/link' import { useState } from 'react' import type { GetPostListRequestQueries } from '@/types/GetPostListRequestQueries' import type { GetPostListResponseBody } from '@/types/GetPostListResponseBody' export async function getServerSideProps (): Promise<GetServerSidePropsResult<{ announcements: GetPostListResponseBody }>> { const queries: GetPostListRequestQueries = { announcement: true, category_based_announcement: false, page: 1 } try { const url = new URL('http://127.0.0.1:5155/forum/posts') const searchParams = new URLSearchParams(queries as Record<string, string>) url.search = searchParams.toString() const response = await fetch(url) const announcements: GetPostListResponseBody = await response.json() return { props: { announcements } } } catch (e) { const response: GetPostListResponseBody = { posts: [] } return { props: { announcements: response } } } } function Forum ({ announcements }: { announcements: GetPostListResponseBody }): JSX.Element { const [page, setPage] = useState(1) const [pageAnnouncements, setPageAnnouncements] = useState(announcements) const fetchAnnouncements = async (): Promise<void> => { const queries: GetPostListRequestQueries = { announcement: true, category_based_announcement: false, page } try { const url = new URL('http://127.0.0.1:5155/forum/posts') const searchParams = new URLSearchParams(queries as Record<string, string>) url.search = searchParams.toString() const response = await fetch(url) const announcements: GetPostListResponseBody = await response.json() setPageAnnouncements(announcements) } catch (e) { const response: GetPostListResponseBody = { posts: [] } setPageAnnouncements(response) } } async function goToPreviousPage (): Promise<void> { setPage(() => page - 1) await fetchAnnouncements() } async function goToNextPage (): Promise<void> { setPage(() => page + 1) await fetchAnnouncements() } return ( <> <Head> <title>Forum • ger</title> <meta name="description" content="reg spelled backwards" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> </Head> <main className="container mx-auto"> <h1 className="text-4xl text-current font-bold">Forum {page}</h1> <div className="flex flex-row justify-between"> <h3 className="text-2xl text-current">Global announcements</h3> <div className="flex flex-row btn-group"> <button className="btn" onClick={goToPreviousPage}>Previous page</button> <button className="btn" onClick={goToNextPage}>Next page</button> </div> </div> <div className="overflow-x-auto"> <table className="table w-full"> <thead> <tr> <td>Topic</td> <td>Replies</td> <td>Views</td> <td>Activity</td> </tr> </thead> <tbody> { pageAnnouncements.posts.map(a => { return ( <tr key={a.id}> <td> <Link className="font-bold link link-hover" href={{ pathname: '/forum/posts/[postId]', query: { postId: a.id } }}>{a.name}</Link> <div className="flex flex-row"> <Link className="text-sm opacity-75 link link-hover" href={{ pathname: '/forum/users/[username]', query: { username: a.username } }}> {a.username} </Link> <p className="text-sm opacity-75"> • {dayjs(a.created_timestamp).format('MMMM D, YYYY HH:mm')} </p> </div> </td> <td>10</td> <td>{a.view_count}</td> <td>1h</td> </tr> ) }) } </tbody> </table> </div> <h3 className="text-2xl text-current font-bold">Trending</h3> <div className="overflow-x-auto"> <table className="table w-full"> <tbody> <tr> <td>Cy Ganderton</td> <td>Quality Control Specialist</td> <td>Blue</td> </tr> </tbody> </table> </div> </main> </> ) } export default Forum
演示动图:
问题原因
React的setState是异步更新状态的,调用setPage后,page变量不会立刻更新为新值。当前代码中,调用setPage后马上执行fetchAnnouncements,此时fetchAnnouncements使用的还是更新前的page值,导致API请求的页码和UI显示的新页码不一致。
修复方案
方案一:直接传递目标页码
计算出目标页码后,同时更新状态和传递给请求函数,确保请求用的是最新页码:
// 修改fetchAnnouncements,接收目标页码参数 const fetchAnnouncements = async (targetPage: number): Promise<void> => { const queries: GetPostListRequestQueries = { announcement: true, category_based_announcement: false, page: targetPage } // 其余请求逻辑不变 } // 修改分页按钮逻辑 async function goToPreviousPage(): Promise<void> { const targetPage = page - 1 if (targetPage < 1) return // 防止页码小于1 setPage(targetPage) await fetchAnnouncements(targetPage) } async function goToNextPage(): Promise<void> { const targetPage = page + 1 setPage(targetPage) await fetchAnnouncements(targetPage) }
方案二:利用setState回调获取最新状态
通过setPage的回调函数获取更新前的状态,计算新页码后直接用于请求:
// 修改fetchAnnouncements,接收目标页码参数 const fetchAnnouncements = async (targetPage: number): Promise<void> => { const queries: GetPostListRequestQueries = { announcement: true, category_based_announcement: false, page: targetPage } // 其余请求逻辑不变 } // 修改分页按钮逻辑 async function goToPreviousPage(): Promise<void> { setPage(prevPage => { const newPage = prevPage - 1 if (newPage >= 1) fetchAnnouncements(newPage) return newPage }) } async function goToNextPage(): Promise<void> { setPage(prevPage => { const newPage = prevPage + 1 fetchAnnouncements(newPage) return newPage }) }
额外优化
- 给上一页按钮添加禁用逻辑,当页码为1时禁用按钮,避免无效请求
- 如果API返回总页数,可以在
goToNextPage中判断是否还有下一页,避免请求不存在的页码
完整修改后的代码
import dayjs from 'dayjs' import type { GetServerSidePropsResult } from 'next' import Head from 'next/head' import Link from 'next/link' import { useState } from 'react' import type { GetPostListRequestQueries } from '@/types/GetPostListRequestQueries' import type { GetPostListResponseBody } from '@/types/GetPostListResponseBody' export async function getServerSideProps (): Promise<GetServerSidePropsResult<{ announcements: GetPostListResponseBody }>> { const queries: GetPostListRequestQueries = { announcement: true, category_based_announcement: false, page: 1 } try { const url = new URL('http://127.0.0.1:5155/forum/posts') const searchParams = new URLSearchParams(queries as Record<string, string>) url.search = searchParams.toString() const response = await fetch(url) const announcements: GetPostListResponseBody = await response.json() return { props: { announcements } } } catch (e) { const response: GetPostListResponseBody = { posts: [] } return { props: { announcements: response } } } } function Forum ({ announcements }: { announcements: GetPostListResponseBody }): JSX.Element { const [page, setPage] = useState(1) const [pageAnnouncements, setPageAnnouncements] = useState(announcements) const fetchAnnouncements = async (targetPage: number): Promise<void> => { const queries: GetPostListRequestQueries = { announcement: true, category_based_announcement: false, page: targetPage } try { const url = new URL('http://127.0.0.1:5155/forum/posts') const searchParams = new URLSearchParams(queries as Record<string, string>) url.search = searchParams.toString() const response = await fetch(url) const announcements: GetPostListResponseBody = await response.json() setPageAnnouncements(announcements) } catch (e) { const response: GetPostListResponseBody = { posts: [] } setPageAnnouncements(response) } } async function goToPreviousPage (): Promise<void> { const targetPage = page - 1 if (targetPage < 1) return setPage(targetPage) await fetchAnnouncements(targetPage) } async function goToNextPage (): Promise<void> { const targetPage = page + 1 // 若API返回总页数,可在此判断:if (targetPage > totalPages) return setPage(targetPage) await fetchAnnouncements(targetPage) } return ( <> <Head> <title>Forum • ger</title> <meta name="description" content="reg spelled backwards" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> </Head> <main className="container mx-auto"> <h1 className="text-4xl text-current font-bold">Forum {page}</h1> <div className="flex flex-row justify-between"> <h3 className="text-2xl text-current">Global announcements</h3> <div className="flex flex-row btn-group"> <button className="btn" onClick={goToPreviousPage} disabled={page === 1}>Previous page</button> <button className="btn" onClick={goToNextPage}>Next page</button> </div> </div> <div className="overflow-x-auto"> <table className="table w-full"> <thead> <tr> <td>Topic</td> <td>Replies</td> <td>Views</td> <td>Activity</td> </tr> </thead> <tbody> { pageAnnouncements.posts.map(a => { return ( <tr key={a.id}> <td> <Link className="font-bold link link-hover" href={{ pathname: '/forum/posts/[postId]', query: { postId: a.id } }}>{a.name}</Link> <div className="flex flex-row"> <Link className="text-sm opacity-75 link link-hover" href={{ pathname: '/forum/users/[username]', query: { username: a.username } }}> {a.username} </Link> <p className="text-sm opacity-75"> • {dayjs(a.created_timestamp).format('MMMM D, YYYY HH:mm')} </p> </div> </td> <td>10</td> <td>{a.view_count}</td> <td>1h</td> </tr> ) }) } </tbody> </table> </div> <h3 className="text-2xl text-current font-bold">Trending</h3> <div className="overflow-x-auto"> <table className="table w-full"> <tbody> <tr> <td>Cy Ganderton</td> <td>Quality Control Specialist</td> <td>Blue</td> </tr> </tbody> </table> </div> </main> </> ) } export default Forum
内容的提问来源于stack exchange,提问作者grindarius
相关产品推荐
相关产品推荐

