Next.js中通过Next.Link切换路由时获取动态参数的正确方式
Next.js动态路由切换时参数获取异常的解决方案
问题场景
开发看板应用时,存在两个看板数据:
{"name": "New Board", "id": "6db0ceec-d371-4b53-8065-2eeebac4694a"} {"name": "tired", "id": "cc41d33e-43a1-49bd-8b76-18e46417b27a"}
通过Next.js的Link组件生成路由链接:
<Link href={`/board/${board.id}`}>{board.name}</Link>
对应创建了动态路由页面src/pages/board/[boardId].js和API接口src/pages/api/board/[boardId].js。服务端渲染时通过getServerSideProps获取数据,客户端切换路由时在useEffect中请求数据,但遇到router.query和router.asPath经常返回旧参数的问题,仅window.location.pathname.split('/')[2]能稳定获取正确值。
页面源码如下:
// src/pages/board/[boardId].js import React, { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import supabase from 'Utilities/SupabaseClient' import Board from 'Components/Screens/Board/Board' import { useRouter } from 'next/router' import axios from 'axios' import { getBaseUrl } from 'Utilities' import { hydrateTasks } from 'Redux/Reducers/TaskSlice' const BoardPage = (props) => { const router = useRouter() const dispatch = useDispatch() async function handleRouteChange() { const { asPath } = router const { boardId } = router.query // 有时不会更新! const idFromWindow = window.location.pathname.split('/')[2] const { board, tasks } = await handleFetchData({boardId: idFromWindow}) console.log(`hello from handleRouteChange:\n\nFrom window: ${idFromWindow}\n\nFrom router.query: ${boardId}\n\nFrom router.asPath: ${asPath}`) dispatch(hydrateTasks({board, tasks})) } useEffect(() => { //subscribe router.events.on('routeChangeComplete', handleRouteChange); //unsubscribe return () => router.events.off('routeChangeComplete', handleRouteChange); }, [ router.events]); return ( <Board {...props}/> ) } const handleFetchData = async ({boardId, req}) => { const baseUrl = getBaseUrl(req) return axios.get(`${baseUrl}/api/board/${boardId}`) .then(({data}) => data) .catch(err => { console.log(err)}) } export async function getServerSideProps ({ query, req }) { const { user } = await supabase.auth.api.getUserByCookie(req) if (!user) { return { props: {}, redirect: { destination: '/signin' } } } const { boardId } = query const { board, tasks} = await handleFetchData({boardId, req}) return { props: { user, board, tasks } } } export default BoardPage
来回切换看板时控制台输出(从"tired"看板开始):
// click 1 [boardId].js?0a51:19 hello from handleRouteChange: From window: 6db0ceec-d371-4b53-8065-2eeebac4694a From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a // click 2 [boardId].js?0a51:19 hello from handleRouteChange: From window: cc41d33e-43a1-49bd-8b76-18e46417b27a From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a // click 3 [boardId].js?0a51:19 hello from handleRouteChange: From window: 6db0ceec-d371-4b53-8065-2eeebac4694a From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a // click 4 [boardId].js?0a51:19 hello from handleRouteChange: From window: cc41d33e-43a1-49bd-8b76-18e46417b27a From router.query: cc41d33e-43a1-49bd-8b76-18e46417b27a From router.asPath: /board/cc41d33e-43a1-49bd-8b76-18e46417b27a
符合Next.js规范的解决方案
方法1:监听router.query.boardId的变化(推荐)
直接将router.query.boardId作为useEffect的依赖项,当路由参数更新时自动触发数据请求,无需监听路由事件,避免闭包导致的旧值问题:
const BoardPage = (props) => { const router = useRouter() const dispatch = useDispatch() useEffect(() => { // 确保boardId存在再执行请求 if (router.query.boardId) { const fetchData = async () => { const { board, tasks } = await handleFetchData({ boardId: router.query.boardId }) dispatch(hydrateTasks({ board, tasks })) } fetchData() } }, [router.query.boardId, dispatch]) // 依赖boardId和dispatch,保证拿到最新值 return ( <Board {...props}/> ) }
方法2:利用routeChangeComplete事件的参数
routeChangeComplete事件会传递当前路由的URL作为参数,直接从该参数解析boardId,绕过闭包中的旧router引用:
const BoardPage = (props) => { const router = useRouter() const dispatch = useDispatch() async function handleRouteChange(url) { // 从事件传递的url中解析boardId const boardId = url.split('/')[2] const { board, tasks } = await handleFetchData({ boardId }) dispatch(hydrateTasks({ board, tasks })) } useEffect(() => { router.events.on('routeChangeComplete', handleRouteChange); return () => router.events.off('routeChangeComplete', handleRouteChange); }, [dispatch]); // 依赖dispatch,避免闭包问题 return ( <Board {...props}/> ) }
问题根源说明
之前的代码中,handleRouteChange函数在组件挂载时创建,闭包捕获了当时的router对象引用。当路由切换后,router内部状态更新,但闭包中的引用并未改变,导致获取到旧的query和asPath值。上述两种方法分别通过监听响应式的router.query变化,或直接使用事件传递的最新URL,解决了闭包导致的旧值问题。
内容的提问来源于stack exchange,提问作者Neil Girardi
相关产品推荐
相关产品推荐

