react-router-dom v6中/hello路径匹配失败问题求助
React Router v6 PrivateRoute 路由匹配失败及测试用例报错解决方案
问题背景
使用react-router-dom v6搭配@types/react-router-dom v5.3.3时,调整代码后出现「No routes matched location "/hello"」警告,且PrivateRoute组件的3个认证后渲染测试用例全部失败,容器内容为空。
主文件代码
import React, { useContext, useEffect } from 'react'; import { Route, RouteProps, Routes, useLocation, useMatch } from 'react-router-dom'; import AuthenticationContext from './AuthenticationContext'; export type PrivateRouteProps = RouteProps & { Element?: React.ComponentType<any>; }; /* eslint-disable react/jsx-props-no-spreading */ function PrivateRoute({ children, path, Element, ...routePropsWithoutChildrenAndComponent }: any) { const { authorize, authenticated, authenticating, callbackPath } = useContext(AuthenticationContext); const location = useLocation(); const match = useMatch(path?.toString() || '*'); useEffect(() => { if (!authenticated && !authenticating && match && location.pathname !== callbackPath) { const authorizeAsync = async () => { authorize(location as unknown as URL); }; authorizeAsync(); } }, [authorize, match, location, authenticated, authenticating, callbackPath]); if (!authenticated) { return null; } return ( <Routes> <Route {...routePropsWithoutChildrenAndComponent} element={Element ? <Element /> : children} /> </Routes> ); } export default PrivateRoute;
测试文件代码
/* eslint-disable react/prop-types */ import React from 'react'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { unstable_HistoryRouter as Router } from 'react-router-dom'; import { PrivateRoute } from '..'; import AuthenticationContext, { AuthenticationContextProps } from '../AuthenticationContext'; describe('PrivateRoute', () => { function renderWithRouterAndContext( content: JSX.Element, { location, context } = {} as { location: string; context: object }, ) { const history = createMemoryHistory({ initialEntries: [location] }); const defaultContext: AuthenticationContextProps = { fetchToken: () => 'xyz' as any, callbackPath: '/oauth', setError: () => {}, authenticated: false, authenticating: false, authorize: () => {}, logout: () => {}, getToken: () => 'xyz', }; const wrapper = ({ children }: { children: React.ReactNode }) => ( <AuthenticationContext.Provider value={{ ...defaultContext, ...context }}> <Router history={history as any}>{children}</Router> </AuthenticationContext.Provider> ); return { ...render(content, { wrapper } as any), history, }; } describe('when authenticated', () => { it('can render children', () => { const { container } = renderWithRouterAndContext( <PrivateRoute path="/hello"> <h1>Hello</h1> </PrivateRoute>, { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } }, ); expect(container.innerHTML).toBe('<h1>Hello</h1>'); }); it('can render a component', () => { function MyComponent() { return <h1>Hey</h1>; } const { container } = renderWithRouterAndContext(<PrivateRoute component={MyComponent} />, { location: '/hello', context: { callbackPath: '/oauth', authenticated: true }, }); expect(container.innerHTML).toBe('<h1>Hey</h1>'); }); it('can invoke a render prop function', () => { const { container } = renderWithRouterAndContext( <PrivateRoute render={({ history, location }: any) => <p>{history.length} {location.pathname}</p>} />, { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } }, ); expect(container.innerHTML).toBe('<p>1 /hello</p>'); }); }); describe('when unauthenticated', () => { const authorize = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); describe('for a matching path', () => { it('checks user authorization and does not render anything', () => { const { container } = renderWithRouterAndContext( <PrivateRoute path="/hello"> <h1>Hello</h1> </PrivateRoute>, { location: '/hello', context: { callbackPath: '/oauth', authenticated: false, authorize }, }, ); expect(container.innerHTML).toBe(''); expect(authorize).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/hello' })); }); }); describe('for an OAuth callback path', () => { it('does not check user authorization and does not render anything', () => { const { container } = renderWithRouterAndContext( <PrivateRoute path="/"> <h1>Hello</h1> </PrivateRoute>, { location: '/oauth?code=xyz&state=foo', context: { callbackPath: '/oauth', authenticated: false, authorize }, }, ); expect(container.innerHTML).toBe(''); expect(authorize).not.toHaveBeenCalled(); }); }); describe('for a non-matching path', () => { it('does not check user authorization', () => { renderWithRouterAndContext( <PrivateRoute path="/hello"> <h1>Hello</h1> </PrivateRoute>, { location: '/hi', context: { callbackPath: '/oauth', authenticated: false, authorize } }, ); expect(authorize).not.toHaveBeenCalled(); }); }); describe('when authentication is in progress', () => { it('does not check user authorization', () => { renderWithRouterAndContext( <PrivateRoute path="/hello"> <h1>Hello</h1> </PrivateRoute>, { location: '/hi', context: { callbackPath: '/hello', authenticating: true, authorize, }, }, ); expect(authorize).not.toHaveBeenCalled(); }); }); }); });
错误信息
- 控制台警告:
No routes matched location "/hello" - 测试用例失败详情:
● PrivateRoute › when authenticated › can render children expect(received).toBe(expected) // Object.is equality Expected: "<h1>Hello</h1>" Received: ""● PrivateRoute › when authenticated › can render a component expect(received).toBe(expected) // Object.is equality Expected: "<h1>Hey</h1>" Received: ""● PrivateRoute › when authenticated › can invoke a render prop function expect(received).toBe(expected) // Object.is equality Expected: "<p>1 /hello</p>" Received: ""
解决方案
1. 重构PrivateRoute组件(适配React Router v6规则)
React Router v6彻底改变了路由嵌套逻辑,私有路由不再需要内部嵌套Routes,而是作为Route的element属性值,同时废弃了v5的component、render属性,统一使用element。
修改后主文件代码:
import React, { useContext, useEffect } from 'react'; import { useLocation, useMatch } from 'react-router-dom'; import AuthenticationContext from './AuthenticationContext'; export type PrivateRouteProps = { element: React.ReactNode; path?: string; }; function PrivateRoute({ element, path }: PrivateRouteProps) { const { authorize, authenticated, authenticating, callbackPath } = useContext(AuthenticationContext); const location = useLocation(); const match = useMatch(path || '*'); useEffect(() => { if (!authenticated && !authenticating && match && location.pathname !== callbackPath) { authorize(location as unknown as URL); } }, [authorize, match, location, authenticated, authenticating, callbackPath]); // 未认证或认证中返回null,保持原逻辑 if (!authenticated || authenticating) { return null; } // 认证通过直接渲染传入的element return element; } export default PrivateRoute;
2. 调整测试用例(适配v6路由写法)
所有路由必须包裹在Routes组件内,且使用element属性传递组件,废弃component、render写法。
修改后测试用例核心部分:
describe('when authenticated', () => { it('can render children', () => { const { container } = renderWithRouterAndContext( <Routes> <Route path="/hello" element={<PrivateRoute element={<h1>Hello</h1>} />} /> </Routes>, { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } }, ); expect(container.innerHTML).toBe('<h1>Hello</h1>'); }); it('can render a component', () => { function MyComponent() { return <h1>Hey</h1>; } const { container } = renderWithRouterAndContext( <Routes> <Route path="/hello" element={<PrivateRoute element={<MyComponent />} />} /> </Routes>, { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } }, ); expect(container.innerHTML).toBe('<h1>Hey</h1>'); }); it('can render element with route props', () => { const TestComponent = ({ location }: { location: Location }) => ( <p>{location.pathname}</p> ); const { container } = renderWithRouterAndContext( <Routes> <Route path="/hello" element={<PrivateRoute element={<TestComponent />} />} /> </Routes>, { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } }, ); expect(container.innerHTML).toBe('<p>/hello</p>'); }); });
3. 其他测试用例调整
未认证相关测试用例同样需要将PrivateRoute包裹在Routes的Route中,确保路由结构符合v6要求。
内容的提问来源于stack exchange,提问作者Chinna P
相关产品推荐
相关产品推荐

