Next.js+getServerSideProps下避免router.asPath对应useEffect首次重复渲染
问题原因
基于getServerSideProps的Next.js Pages Router页面首次加载时,router.asPath会触发两次变更:
- 第一次是服务端直出HTML完成初始渲染时,路由对象初始化触发asPath更新
- 第二次是客户端水合完成、路由系统完全激活时,会再次同步路由状态触发asPath更新
两次变更都会触发绑定了[router.asPath]依赖的useEffect执行,最终导致重复的数据请求。
解决思路
方案1(推荐,符合Next.js设计逻辑):首屏数据走服务端预取,客户端只处理后续路由变更
既然已经使用getServerSideProps,完全可以把首屏的参数解析、数据拉取逻辑迁移到服务端执行,从根源避免首屏重复请求:
- 在
getServerSideProps内直接从context中解析query参数,在服务端完成数据库查询,将解析后的筛选条件、首屏数据通过props传递给页面组件 - 页面组件初始化时,直接将props中的筛选条件作为
filtersData的初始值,首屏直接渲染服务端返回的预取数据,不需要在首屏执行客户端拉取 - 组件内增加首屏挂载标记,跳过首次effect执行,只在用户操作筛选器触发客户端路由跳转时,才执行客户端参数解析和数据拉取
核心实现参考:
import { useRef, useState } from 'react' import { useRouter } from 'next/router' export default function ExplorePage({ initialFilters, initialData }) { const router = useRouter() // 首屏挂载标记 const isFirstLoad = useRef(true) // 初始化状态直接用服务端下发的数据 const [filtersData, setFiltersData] = useState(initialFilters) const [listData, setListData] = useState(initialData) const [initialLoading, setInitialLoading] = useState(false) useEffect(() => { // 首屏由getServerSideProps已经拿到数据,直接跳过 if (isFirstLoad.current) { isFirstLoad.current = false return } // 直接用router.query拿解析好的参数,不用自己手写解析逻辑 const result = router.query const queryParams = [] // 合并为一次setState,避免多次重渲染 const newFilters = { ...filtersData, thumbType: result.type } if (result.field) { newFilters.categoryField = result.field newFilters.categoryOperator = result.fieldOp newFilters.categoryValue = result.fieldVal queryParams.push(where(result.field, result.fieldOp, decodeURIComponent(result.fieldVal))) } if (result.orderBy) { newFilters.orderFieldActions = result.orderBy newFilters.orderOperatorActions = result.orderType queryParams.push(orderBy(result.orderBy, result.orderType)) } setFiltersData(newFilters) setSearchParams(queryParams) getFilteredData(result.type).then(res => setListData(res)) setInitialLoading(false) }, [router.asPath]) // 页面渲染逻辑 } export async function getServerSideProps(context) { // 在这里解析query、查库,返回初始数据给页面 const { type, category, field, fieldOp, fieldVal, orderBy, orderType } = context.query // 执行对应数据库查询逻辑拿到initialData // 组装初始筛选参数initialFilters return { props: { initialFilters, initialData } } }
方案2:请求参数去重(不调整getServerSideProps逻辑的快速修复方案)
如果暂时不想迁移逻辑到服务端,可以通过缓存已请求的参数标识,两次effect触发时如果参数完全一致,直接跳过重复请求:
const lastRequestQuery = useRef('') useEffect(() => { const result = router.query // 替换自己手写的解析逻辑 // 生成当前请求参数的唯一标识 const currentQueryKey = JSON.stringify(result) // 和上一次请求的参数对比,完全一致就终止执行 if (currentQueryKey === lastRequestQuery.current) return lastRequestQuery.current = currentQueryKey // 原有参数更新、数据拉取逻辑 // ... }, [router.asPath])
额外优化建议
- 不要手动解析
location.search,Next.js的router.query已经内置了完整的参数解析能力,能覆盖编码异常、空参数、多值参数等边界场景 - 不要在同一个effect内多次调用
setFiltersData,多次状态更新会触发不必要的组件重渲染,将所有状态变更合并为一次setState调用即可 - 数据请求逻辑建议增加中断控制,用户快速切换筛选条件时取消上一次未完成的请求,避免旧请求返回覆盖最新的渲染结果
内容的提问来源于stack exchange,提问作者Maxime Crtgn
相关产品推荐
相关产品推荐

