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

如何修改数据获取策略避免Next.js动态路由初始渲染旧数据?

问题:切换看板时短暂显示旧数据的优化方案

我开发了一个支持多看板创建和查看的Next.js应用,用户可通过首页或主导航的看板列表项(均使用Next.js Link组件)进入动态页面src/pages/board/[boardId].js。该页面通过getServerSideProps()获取看板数据,路由变化时触发effect更新Redux store,Board组件通过useSelector()从Redux拉取数据渲染。

当前问题:在不同看板间切换时,会短暂显示上一个看板的数据,再加载当前看板数据,需优化该体验。


优化方案

1. 直接传递服务端获取的props(推荐)

getServerSideProps已在服务端拿到当前看板的完整数据,直接将props传给Board组件,跳过Redux中间环节,避免状态更新延迟。路由切换时新页面的props会直接驱动组件渲染,不会残留旧数据。

2. 路由切换时清空Redux状态

若坚持使用Redux,可监听路由开始变化的事件,提前清空看板相关状态,同时在组件中判断数据加载状态,未完成时显示加载占位符。

3. 路由预加载(辅助优化)

对用户高频访问的看板,使用Link的prefetch属性提前预加载数据,减少切换等待时间,但需配合前两种方案解决旧数据显示问题。


代码实现示例

方案1:直接传递props(移除Redux依赖)

src/pages/board/[boardId].js

import React from 'react'
import Board from 'Components/Board/Board'
import { unstable_getServerSession } from 'next-auth/next'
import { authOptions } from 'pages/api/auth/[...nextauth]'
import prisma from 'Utilities/PrismaClient'

const BoardPage = ({ board, tasks }) => {
  // 直接将服务端获取的props传入Board组件
  return <Board board={board} tasks={tasks} />
}

export async function getServerSideProps ({ query, req, res }) {
  const session = await unstable_getServerSession(req, res, authOptions)
  if (!session) {
    return {
      redirect: { destination: '/signin', permanent: false }
    }
  }
  const { boardId } = query

  const boardQuery = prisma.board.findUnique({
    where: { id: boardId },
    select: { name: true, description: true, id: true, TO_DO: true, IN_PROGRESS: true, DONE: true }
  })

  const taskQuery = prisma.task.findMany({
    where: { board: boardId },
    select: { id: true, title: true, description: true, status: true, board: true }
  })

  try {
    const [board, tasks] = await prisma.$transaction([boardQuery, taskQuery])
    return { props: { board, tasks } }
  } catch (error) {
    console.log(error)
    return { props: { board: {}, tasks: [] } }
  }
}

export default BoardPage

src/Components/Board/Board.js

import { useEffect } from 'react'
import { useStyletron } from 'baseui'
import Column from 'Components/Column/Column'
import ErrorBoundary from 'Components/ErrorBoundary/ErrorBoundary'
import useExpiringBoolean from 'Hooks/useExpiringBoolean'
import { DragDropContext } from 'react-beautiful-dnd'
import Confetti from 'react-confetti'
import { useDispatch } from 'react-redux'
import useWindowSize from 'react-use/lib/useWindowSize'
import { moveTask } from 'Redux/Reducers/TaskSlice'
import { handleDragEnd } from './BoardUtilities'
import { StyledBoardMain } from './style'

const Board = ({ board, tasks }) => {
  const [css, theme] = useStyletron()
  const dispatch = useDispatch()

  useEffect(() => {
    document.querySelector('body').style.background = theme.colors.backgroundPrimary
  }, [theme])

  // 从props解析数据,替代Redux的useSelector
  const boardName = board.name
  const boardDescription = board.description
  const columnOrder = ['TO_DO', 'IN_PROGRESS', 'DONE']
  const columns = {
    TO_DO: {
      title: '待办',
      status: 'TO_DO',
      taskIds: tasks.filter(t => t.status === 'TO_DO').map(t => t.id)
    },
    IN_PROGRESS: {
      title: '进行中',
      status: 'IN_PROGRESS',
      taskIds: tasks.filter(t => t.status === 'IN_PROGRESS').map(t => t.id)
    },
    DONE: {
      title: '已完成',
      status: 'DONE',
      taskIds: tasks.filter(t => t.status === 'DONE').map(t => t.id)
    }
  }
  const taskMap = tasks.reduce((acc, task) => {
    acc[task.id] = task
    return acc
  }, {})

  const { boolean: showDone, useTrigger: doneUseTrigger } = useExpiringBoolean({ defaultState: false })
  const doneTrigger = doneUseTrigger({ duration: 4000 })
  const { width, height } = useWindowSize()
  const curriedDragEnd = handleDragEnd({ dispatch, action: moveTask, handleOnDone: doneTrigger })

  // 加载状态判断
  if (!board.id || tasks.length === 0) {
    return (
      <div className={css({
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '80vh',
        fontSize: '18px'
      })}>
        加载中...
      </div>
    )
  }

  return (
    <ErrorBoundary>
      <DragDropContext onDragEnd={curriedDragEnd}>
        <div className={css({
          marginLeft: '46px',
          marginTop: '16px',
          fontFamily: 'Roboto',
          display: 'flex',
          alignItems: 'baseline'
        })}>
          <h1 className={css({ fontSize: '22px', color: theme.colors.primary })}>{boardName}</h1>
          {boardDescription && (
            <p className={css({ marginLeft: '10px', color: theme.colors.primary })}>{boardDescription}</p>
          )}
        </div>
        <StyledBoardMain>
          {columnOrder.map(columnKey => {
            const column = columns[columnKey]
            const tasksArray = column.taskIds.map(taskId => taskMap[taskId])
            return (
              <Column
                column={columnKey}
                key={`COLUMN_${columnKey}`}
                tasks={tasksArray}
                title={column.title}
                status={column.status}
              />
            )
          })}
        </StyledBoardMain>
      </DragDropContext>
      {showDone && <Confetti width={width} height={height} />}
    </ErrorBoundary>
  )
}

export default Board

方案2:路由切换时清空Redux状态

src/pages/board/[boardId].js(修改部分)

import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Board from 'Components/Board/Board'
import { useRouter } from 'next/router'
import { hydrateTasks, clearBoardData } from 'Redux/Reducers/TaskSlice'
// ... 其他导入不变

const BoardPage = ({ board, tasks }) => {
  const router = useRouter()
  const dispatch = useDispatch()

  // 路由开始切换时清空Redux状态
  useEffect(() => {
    const handleRouteChangeStart = () => dispatch(clearBoardData())
    router.events.on('routeChangeStart', handleRouteChangeStart)
    return () => router.events.off('routeChangeStart', handleRouteChangeStart)
  }, [router, dispatch])

  useEffect(() => {
    dispatch(hydrateTasks({ board, tasks }))
  }, [router, board, tasks])

  return <Board />
}

// getServerSideProps部分不变

src/Redux/Reducers/TaskSlice.js(新增清空逻辑)

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  boardName: '',
  boardDescription: '',
  columnOrder: [],
  columns: {},
  tasks: {}
}

const taskSlice = createSlice({
  name: 'task',
  initialState,
  reducers: {
    hydrateTasks: (state, action) => {
      // 原有hydrate逻辑不变
    },
    clearBoardData: () => initialState, // 重置为初始状态
    moveTask: (state, action) => {
      // 原有moveTask逻辑不变
    }
  }
})

export const { hydrateTasks, clearBoardData, moveTask } = taskSlice.actions
export default taskSlice.reducer

src/Components/Board/Board.js(添加加载状态)

const { boardDescription, boardName, columnOrder, columns, tasks } = useSelector(state => state?.task)

// 加载状态判断
if (!boardName || Object.keys(tasks).length === 0) {
  return (
    <div className={css({
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      height: '80vh',
      fontSize: '18px'
    })}>
      加载中...
    </div>
  )
}

内容的提问来源于stack exchange,提问作者Neil Girardi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 00:37:08