如何修改数据获取策略避免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
相关产品推荐
相关产品推荐

