Skip to content

在 React Router v6 中使用历史记录

问题描述

在使用 React Router v6 时,很多开发者发现 this.props.history.push('/path') 不再工作。尝试使用 createBrowserHistory() 创建自定义历史记录对象虽然可以改变 URL,但页面不会实际刷新或导航到新位置。

以下是典型的问题代码示例:

js
const history = createBrowserHistory();
history.push('/UserDashboard'); // URL 变化但页面不刷新

为什么会出现这个问题

React Router v6 进行了重大架构变更,移除了之前的 history API,改用新的 useNavigate hook。这是为了提供更一致的 API 设计并简化导航操作。

解决方案

1. 使用 useNavigate Hook(推荐)

在函数组件中,使用 useNavigate hook 替代 useHistory

js
import { useNavigate } from 'react-router-dom';

function MyComponent() {
  const navigate = useNavigate();
  
  const handleClick = () => {
    navigate('/target-path'); // 基本导航
    navigate('/target-path', { replace: true }); // 替换当前历史记录
    navigate(-1); // 返回上一页
    navigate(1); // 前进一页
  };
  
  return (
    <button onClick={handleClick}>导航</button>
  );
}

TIP

useNavigate 返回的函数比之前的 history API 更简洁,不需要调用 .push().replace() 方法。

2. 在类组件中使用导航

如果你仍在使用的类组件,可以通过以下方式使用导航功能:

方法一:创建高阶组件包装器

js
// withRouter.js
import React from 'react';
import { useNavigate } from 'react-router-dom';

const withRouter = WrappedComponent => props => {
  const navigate = useNavigate();
  return <WrappedComponent {...props} navigate={navigate} />;
};

export default withRouter;
js
// 类组件中使用
import withRouter from './withRouter';

class MyClassComponent extends React.Component {
  handleSubmit = (e) => {
    e.preventDefault();
    this.props.navigate('/target-path');
  };
  
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        {/* 表单内容 */}
      </form>
    );
  }
}

export default withRouter(MyClassComponent);

方法二:使用函数包装器

js
import { useNavigate } from 'react-router-dom';

class MyClassComponent extends React.Component {
  // 组件逻辑
}

export default function(props) {
  const navigate = useNavigate();
  return <MyClassComponent {...props} navigate={navigate} />;
}

3. 在 React 上下文外使用导航

如果需要在外部的非 React 函数(如 API 调用处理)中使用导航,可以创建一个全局导航引用:

js
// navigation.js
let navigateRef;

export const navigate = (...args) => {
  if (navigateRef) {
    navigateRef(...args);
  }
};

export const useNavigateSync = () => {
  navigateRef = useNavigate();
};
js
// App.js 或顶级组件
import { useNavigateSync } from './navigation';

function App() {
  useNavigateSync();
  // 应用的其他内容
}
js
// 在任何地方使用
import { navigate } from './navigation';

// API 调用后导航
apiCall().then(() => {
  navigate('/success-page');
});

注意事项

这种方法需要在导航可用后使用(即应用已渲染),在应用初始化期间调用可能导致错误。

4. 自定义路由器(高级用法)

如果需要完全控制路由历史记录,可以创建自定义路由器:

js
import { Router } from 'react-router-dom';
import { useLayoutEffect, useState } from 'react';

const CustomRouter = ({ history, children }) => {
  const [state, setState] = useState({
    action: history.action,
    location: history.location,
  });

  useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      location={state.location}
      navigationType={state.action}
      navigator={history}
      children={children}
    />
  );
};
js
// 应用中使用
import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

ReactDOM.render(
  <CustomRouter history={history}>
    <App />
  </CustomRouter>,
  document.getElementById('root')
);

迁移指南

从 React Router v5 迁移到 v6 时,注意以下变化:

v5v6
useHistory()useNavigate()
history.push()navigate()
history.replace()navigate(path, { replace: true })
history.goBack()navigate(-1)
history.goForward()navigate(1)

常见问题解答

为什么我的导航只改变 URL 但不渲染组件?

这通常是因为你创建了多个历史记录实例。React Router 应该管理单一的历史记录对象,自定义历史记录实例不会与路由器的状态同步。

我可以在生命周期方法中使用 useNavigate 吗?

不可以,useNavigate 只能在函数组件或自定义 Hook 中使用。对于类组件,请使用上述的包装器模式。

如何处理异步操作后的导航?

最佳实践是在异步操作完成后调用导航:

js
const handleSubmit = async (event) => {
  event.preventDefault();
  
  try {
    const result = await apiCall();
    navigate('/success');
  } catch (error) {
    // 处理错误
  }
};

总结

React Router v6 引入了 useNavigate hook 来替代之前的 history API,这提供了更简洁和一致的导航方式。虽然这需要一些迁移工作,但新的 API 设计更加直观和易于使用。对于类组件,可以使用高阶组件或包装器模式来访问导航功能。