You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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">
                             &nbsp;•&nbsp;{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">
                             &nbsp;•&nbsp;{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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 12:01:00