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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 22:05:20