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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 10:59:50