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

NextJS13中导入next/router的SingletonRouter为Null问题求助

解决Next.js中未保存修改时阻止路由变更的问题

你遇到的next/router单例返回null的情况,是因为Next.js 13+的App Router(app目录)不兼容旧的next/router API。旧路由系统仅适用于Pages Router(pages目录),下面分两种路由场景给出具体解决方案:


一、Pages Router(pages目录)场景

在Pages Router中,正确使用next/router拦截路由变更的方式如下:

实现代码

import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';

const EditPage = () => {
  const router = useRouter();
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

  // 拦截内部路由跳转
  useEffect(() => {
    const handleRouteChange = (targetUrl) => {
      if (hasUnsavedChanges) {
        // 取消当前路由变更
        router.events.emit('routeChangeError', new Error('导航已取消'), targetUrl);
        
        if (window.confirm('你有未保存的修改,确定要离开吗?')) {
          // 用户确认后允许跳转
          router.push(targetUrl);
        }
      }
    };

    router.events.on('routeChangeStart', handleRouteChange);
    // 组件卸载时移除监听,避免内存泄漏
    return () => router.events.off('routeChangeStart', handleRouteChange);
  }, [hasUnsavedChanges, router]);

  // 拦截页面刷新/关闭
  useEffect(() => {
    const handleBeforeUnload = (e) => {
      if (hasUnsavedChanges) {
        e.preventDefault();
        e.returnValue = '你有未保存的修改,确定要离开吗?';
      }
    };

    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
  }, [hasUnsavedChanges]);

  return (
    <div>
      <input
        type="text"
        onChange={() => setHasUnsavedChanges(true)}
        placeholder="输入内容..."
      />
      <button onClick={() => setHasUnsavedChanges(false)}>保存修改</button>
    </div>
  );
};

export default EditPage;

二、App Router(app目录)场景

App Router使用next/navigation提供的新路由API,没有全局Router单例,需要通过自定义导航拦截+状态共享实现需求:

1. 创建未保存状态上下文

// app/contexts/UnsavedChangesContext.jsx
'use client';

import { createContext, useState, useContext } from 'react';

const UnsavedChangesContext = createContext();

export const UnsavedChangesProvider = ({ children }) => {
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
  return (
    <UnsavedChangesContext.Provider value={{ hasUnsavedChanges, setHasUnsavedChanges }}>
      {children}
    </UnsavedChangesContext.Provider>
  );
};

export const useUnsavedChanges = () => useContext(UnsavedChangesContext);

2. 自定义拦截式Link组件

// app/components/CustomLink.jsx
'use client';

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useUnsavedChanges } from '../contexts/UnsavedChangesContext';

const CustomLink = ({ href, children, ...props }) => {
  const router = useRouter();
  const { hasUnsavedChanges } = useUnsavedChanges();

  const handleClick = (e) => {
    if (hasUnsavedChanges) {
      e.preventDefault();
      if (window.confirm('你有未保存的修改,确定要离开吗?')) {
        router.push(href);
      }
    }
  };

  return (
    <Link href={href} onClick={handleClick} {...props}>
      {children}
    </Link>
  );
};

export default CustomLink;

3. 封装安全导航函数(处理程序式跳转)

// app/utils/safeNavigation.js
'use client';

import { useRouter } from 'next/navigation';
import { useUnsavedChanges } from '../contexts/UnsavedChangesContext';

export const useSafeNavigation = () => {
  const router = useRouter();
  const { hasUnsavedChanges } = useUnsavedChanges();

  const push = (href) => {
    if (hasUnsavedChanges) {
      window.confirm('你有未保存的修改,确定要离开吗?') && router.push(href);
    } else {
      router.push(href);
    }
  };

  const replace = (href) => {
    if (hasUnsavedChanges) {
      window.confirm('你有未保存的修改,确定要离开吗?') && router.replace(href);
    } else {
      router.replace(href);
    }
  };

  return { push, replace };
};

4. 拦截浏览器前进后退

// app/components/UnsavedChangesHandler.jsx
'use client';

import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { useUnsavedChanges } from '../contexts/UnsavedChangesContext';

const UnsavedChangesHandler = () => {
  const { hasUnsavedChanges } = useUnsavedChanges();
  const currentPath = usePathname();

  // 拦截浏览器前进后退
  useEffect(() => {
    const handlePopState = () => {
      if (hasUnsavedChanges) {
        // 重置路由到当前页面,阻止默认回退
        history.pushState(null, '', currentPath);
        if (window.confirm('你有未保存的修改,确定要离开吗?')) {
          history.back();
        }
      }
    };

    window.addEventListener('popstate', handlePopState);
    return () => window.removeEventListener('popstate', handlePopState);
  }, [hasUnsavedChanges, currentPath]);

  // 拦截页面刷新/关闭
  useEffect(() => {
    const handleBeforeUnload = (e) => {
      if (hasUnsavedChanges) {
        e.preventDefault();
        e.returnValue = '你有未保存的修改,确定要离开吗?';
      }
    };

    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => window.removeEventListener('beforeunload', handleBeforeUnload);
  }, [hasUnsavedChanges]);

  return null;
};

export default UnsavedChangesHandler;

5. 在根布局中注册上下文和处理器

// app/layout.jsx
import { UnsavedChangesProvider } from './contexts/UnsavedChangesContext';
import UnsavedChangesHandler from './components/UnsavedChangesHandler';

export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body>
        <UnsavedChangesProvider>
          <UnsavedChangesHandler />
          {children}
        </UnsavedChangesProvider>
      </body>
    </html>
  );
}

6. 在表单页面使用状态

// app/edit/page.jsx
'use client';

import { useUnsavedChanges } from '../contexts/UnsavedChangesContext';
import CustomLink from '../components/CustomLink';
import { useSafeNavigation } from '../utils/safeNavigation';

const EditPage = () => {
  const { setHasUnsavedChanges } = useUnsavedChanges();
  const { push } = useSafeNavigation();

  return (
    <div>
      <input
        type="text"
        onChange={() => setHasUnsavedChanges(true)}
        placeholder="输入内容..."
      />
      <button onClick={() => setHasUnsavedChanges(false)}>保存修改</button>
      
      {/* 使用自定义Link */}
      <CustomLink href="/">返回首页</CustomLink>
      
      {/* 使用安全导航函数处理程序式跳转 */}
      <button onClick={() => push('/')}>程序式返回首页</button>
    </div>
  );
};

export default EditPage;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 09:52:02