发布至npm的可复用React Dashboard组件中useNavigate使用报错的解决方案咨询
我最近踩了个坑——自己开发的可复用React Dashboard组件包,在本地项目直接引用时用useNavigate完全正常,但发布到npm后,其他开发者安装使用就会抛出Uncaught Error: useNavigate() may be used only in the context of a <Router> component的错误,折腾了好一阵才捋清楚原因和解决办法,这里分享给大家。
问题场景还原
我开发的leximo-dashboard包内有个LeftMenu组件,内部依赖react-router-dom的useNavigate钩子处理菜单导航,本地直接调试组件时毫无问题:
// 组件内部LeftMenu核心代码 import { useNavigate } from 'react-router-dom'; const LeftMenu = ({ menuItems }) => { const navigate = useNavigate(); const handleMenuClick = (item) => { // 把navigate实例传给外部传入的action item.action({ navigate, closeMenu: () => {/* 菜单关闭逻辑 */} }); }; // ...组件渲染逻辑 };
但当用户从npm安装后,在他们的项目中按常规方式使用就会报错,用户的项目代码示例如下:
// 用户项目的App.js代码 import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { Dashboard, ThemeProvider } from 'leximo-dashboard'; import 'leximo-dashboard/dist/assets/style.css'; import AuthProvider from "./Providers/AuthProvider"; export default function App() { return ( <AuthProvider> <ThemeProvider> <Router> <AppContent /> </Router> </ThemeProvider> </AuthProvider> ); } function AppContent() { const { user } = useAuth(); const menuItems = [ { label: 'Home', icon: 'home', action: ({ navigate }) => navigate('/') } ]; return ( <Routes> <Route path="/" element={<Dashboard menuItems={menuItems} />} /> {/* 其他路由配置 */} </Routes> ); }
问题根源
这个错误的核心原因是React Router的上下文隔离:
我最初把react-router-dom放在了组件包的dependencies里,这会导致组件包打包时把react-router-dom的代码一起打进包内。当用户的项目也安装了react-router-dom时,就会出现两个不同的react-router-dom实例——组件包用自己的实例,用户项目用本地的实例。
而useNavigate必须在同一个Router实例的上下文中才能工作,组件包内部的useNavigate找不到用户项目中Router提供的上下文,自然就抛出了错误。
可行解决方案
方案1:将react-router-dom设为peerDependencies(推荐)
这是最标准的解决方案,可复用组件不应该自带react-router-dom这类全局依赖,而是要求宿主项目提供兼容的版本,确保组件和宿主项目共享同一个依赖实例。
具体操作:
- 修改组件包的
package.json,把react-router-dom从dependencies移到peerDependencies,同时指定兼容的版本范围:
{ "name": "leximo-dashboard", // ...其他配置 "peerDependencies": { "react-router-dom": "^6.0.0" }, "devDependencies": { "react-router-dom": "^6.0.0" // 开发调试时需要安装 } }
- 重新执行
npm install,然后重新打包组件并发布到npm。
这样用户安装你的组件时,npm会自动检查他们项目中是否有符合版本要求的react-router-dom,如果没有会提示用户安装,确保组件和用户项目使用同一个依赖实例,上下文就能正常共享了。
方案2:让用户传入navigate实例(降级/灵活方案)
如果需要让组件支持不依赖react-router-dom的场景,或者给用户更多灵活性,可以把navigate实例作为props从外部传入组件,而不是在组件内部使用useNavigate。
修改组件内部的LeftMenu和Dashboard:
// 改造后的LeftMenu,从props接收navigate const LeftMenu = ({ menuItems, navigate }) => { const handleMenuClick = (item) => { item.action({ navigate, closeMenu: () => {/* 菜单关闭逻辑 */} }); }; // ...渲染逻辑 }; // 对应的Dashboard组件也需要接收navigate并传递给LeftMenu const Dashboard = ({ menuItems, topBarConfig, navigate }) => { return ( <div className="dashboard-container"> <LeftMenu menuItems={menuItems} navigate={navigate} /> {/* 顶部栏等其他组件 */} </div> ); };
用户在使用时,自己通过useNavigate拿到实例后传入Dashboard:
// 用户项目的AppContent组件 import { useNavigate } from 'react-router-dom'; function AppContent() { const { user } = useAuth(); const navigate = useNavigate(); const menuItems = [ { label: 'Home', icon: 'home', action: ({ navigate }) => navigate('/') } ]; return ( <Routes> <Route path="/" element={<Dashboard menuItems={menuItems} navigate={navigate} />} /> </Routes> ); }
这个方案虽然增加了用户的使用成本,但能让组件脱离react-router-dom的上下文限制,适合需要更灵活导航逻辑的场景。
总结
遇到这类依赖上下文的报错,首先要想到是否存在多依赖实例的问题。对于React Router这类需要共享上下文的库,将其设为peerDependencies是可复用组件的标准做法,能从根源上解决上下文隔离的问题。
内容来源于stack exchange

