使用 React router V4 以编程方式导航

Programmatically navigate using react router V4

我刚刚将 react-router 从 v3 替换为 v4。
但我不确定如何以编程方式在 Component 的成员函数中导航。 即在 handleClick() 函数中,我想在处理一些数据后导航到 /path/some/where。 我曾经这样做:

import { browserHistory } from 'react-router'
browserHistory.push('/path/some/where')

但是我在v4中找不到这样的接口
如何使用 v4 导航?

如果您的目标是浏览器环境,则需要使用 react-router-dom 包,而不是 react-router。他们采用与 React 相同的方法,以便将核心 (react) 和平台特定代码 (react-dom, react-native) 分开' 需要安装两个单独的包,因此环境包包含您需要的一切。您可以将其添加到您的项目中:

yarn add react-router-dom

npm i react-router-dom

您需要做的第一件事是提供一个 <BrowserRouter> 作为应用程序中最顶层的父组件。 <BrowserRouter> 使用 HTML5 history API 并为您管理它,因此您不必担心自己实例化它并将其传递给 <BrowserRouter> 组件作为道具(就像你在以前的版本中需要做的那样)。

在 V4 中,为了以编程方式导航,您需要访问 history 对象,只要您有 <BrowserRouter> ,就可以通过 React context 获得该对象provider 组件作为应用程序中最顶层的父组件。该库通过上下文公开 router 对象,该对象本身包含 history 作为 属性。 history 界面提供了多种导航方法,例如 pushreplacegoBack 等。您可以检查整个属性和方法列表 here.

给 Redux/Mobx 用户的重要提示

如果您在应用程序中使用 redux 或 mobx 作为状态管理库,您可能会遇到组件问题,这些组件应该是 location-aware 而不是 re-rendered 在触发 URL更新

发生这种情况是因为 react-router 使用上下文模型将 location 传递给组件。

Both connect and observer create components whose shouldComponentUpdate methods do a shallow comparison of their current props and their next props. Those components will only re-render when at least one prop has changed. This means that in order to ensure they update when the location changes, they will need to be given a prop that changes when the location changes.

解决此问题的 2 种方法是:

  • 将您的 connected 组件包装在无路径 <Route /> 中。当前 location 对象是 <Route> 传递给它呈现的组件的道具之一
  • withRouter higher-order 组件包装你的 connected 组件,实际上具有相同的效果并注入 location 作为道具

撇开这个不谈,有四种以编程方式导航的方法,按推荐排序:

1.- 使用 <Route> 组件

它提倡声明式风格。在 v4 之前,<Route /> 组件被放置在组件层次结构的顶部,必须事先考虑您的路由结构。但是,现在您可以在树中的任何地方 <Route> 组件 ,允许您根据 URL 更好地控制有条件的渲染。 Routematchlocationhistory 作为属性注入到您的组件中。导航方法(例如 pushreplacegoBack...)可用作 history 对象的属性。

有 3 种方法可以通过使用 componentrenderchildren 道具来渲染带有 Route 的内容,但不要使用超过一种在同一个Route。选择取决于用例,但基本上前两个选项只会在 path 与 url 位置匹配时呈现您的组件,而对于 children 组件将呈现路径是否匹配位置(用于根据 URL 匹配调整 UI)。

如果你想自定义你的组件渲染输出,你需要将你的组件包装在一个函数中并使用render选项,以便传递给你的除了 matchlocationhistory 之外,还可以添加您想要的任何其他道具。举例说明:

import { BrowserRouter as Router } from 'react-router-dom'

const ButtonToNavigate = ({ title, history }) => (
  <button
    type="button"
    onClick={() => history.push('/my-new-location')}
  >
    {title}
  </button>
);

const SomeComponent = () => (
  <Route path="/" render={(props) => <ButtonToNavigate {...props} title="Navigate elsewhere" />} />
)    

const App = () => (
  <Router>
    <SomeComponent /> // Notice how in v4 we can have any other component interleaved
    <AnotherComponent />
  </Router>
);

2.- 使用withRouter HoC

这个高阶组件将注入与 Route 相同的道具。但是,它具有每个文件只能有 1 个 HoC 的限制。

import { withRouter } from 'react-router-dom'

const ButtonToNavigate = ({ history }) => (
  <button
    type="button"
    onClick={() => history.push('/my-new-location')}
  >
    Navigate
  </button>
);


ButtonToNavigate.propTypes = {
  history: React.PropTypes.shape({
    push: React.PropTypes.func.isRequired,
  }),
};

export default withRouter(ButtonToNavigate);

3.- 使用 Redirect 组件

呈现 <Redirect> 将导航到新位置。但请记住,默认情况下,当前位置被新位置替换,如server-side 重定向(HTTP 3xx)。新位置由 to 属性提供,它可以是一个字符串(URL 重定向到)或一个 location 对象。如果您想 将新条目推送到历史记录中 ,请同时传递 push 属性并将其设置为 true

<Redirect to="/your-new-location" push />

4.- 通过 context

手动访问 router 有点气馁,因为 context 仍然是一个实验性的 API 并且很可能break/change 在 React 的未来版本中

const ButtonToNavigate = (props, context) => (
  <button
    type="button"
    onClick={() => context.router.history.push('/my-new-location')}
  >
    Navigate to a new location
  </button>
);

ButtonToNavigate.contextTypes = {
  router: React.PropTypes.shape({
    history: React.PropTypes.object.isRequired,
  }),
};

不用说,还有其他 Router 组件适用于非浏览器生态系统,例如 <NativeRouter> 复制导航堆栈 在内存中 和目标React Native 平台,可通过 react-router-native 包获取。

如需任何进一步的参考,请不要犹豫,查看由库的 co-authors 之一制作的 official docs. There is also a video,它提供了对 react-router 的非常酷的介绍v4,突出了一些主要变化。

我在迁移到 React-Router v4 时遇到了类似的问题,所以我将在下面尝试解释我的解决方案。

请不要将此答案视为解决问题的正确方法,我想随着 React Router v4 变得更加成熟并离开测试版,很有可能会出现更好的东西(它甚至可能已经存在,我只是没有发现它)。

对于上下文,我遇到这个问题是因为我偶尔使用 Redux-Saga 以编程方式更改历史对象(比如用户成功验证时)。

在 React Router 文档中,查看 <Router> component,您会发现您可以通过 prop 传递自己的历史记录对象。这是解决方案的本质 - 我们从 global[=49 向 React-Router 提供历史对象 =]模块。

步骤:

  1. 安装历史npm模块-yarn add historynpm install history --save
  2. 在您的 App.js 级别文件夹中创建一个名为 history.js 的文件(这是我的偏好)

    // src/history.js
    
    import createHistory from 'history/createBrowserHistory';
    export default createHistory();`
    
  3. 像这样将这个历史对象添加到你的 Router 组件

    // src/App.js
    
    import history from '../your/path/to/history.js;'
    <Router history={history}>
    // Route tags here
    </Router>
    
  4. 像以前一样通过导入你的全局历史对象来调整URL:

    import history from '../your/path/to/history.js;'
    history.push('new/path/here/');
    

现在一切都应该保持同步,而且您还可以访问以编程方式设置历史对象的方法,而不是通过 component/container。

我已经测试 v4 几天了......到目前为止我很喜欢它!一段时间后才有意义。

我也有同样的问题,我发现像下面这样处理它效果最好(甚至可能是它的预期方式)。它使用状态、三元运算符和 <Redirect>.

在构造函数中()

this.state = {
    redirectTo: null
} 
this.clickhandler = this.clickhandler.bind(this);

在 render() 中

render(){
    return (
        <div>
        { this.state.redirectTo ?
            <Redirect to={{ pathname: this.state.redirectTo }} /> : 
            (
             <div>
               ..
             <button onClick={ this.clickhandler } />
              ..
             </div>
             )
         }

在 clickhandler() 中

 this.setState({ redirectTo: '/path/some/where' });

希望对您有所帮助。让我知道。

TL;DR:

if (navigate) {
  return <Redirect to="/" push={true} />
}

简单而明确的答案是您需要将 <Redirect to={URL} push={boolean} />setState()

结合使用

push: boolean - when true, redirecting will push a new entry onto the history instead of replacing the current one.


import { Redirect } from 'react-router'

class FooBar extends React.Component {
  state = {
    navigate: false
  }

  render() {
    const { navigate } = this.state

    // here is the important part
    if (navigate) {
      return <Redirect to="/" push={true} />
    }
   // ^^^^^^^^^^^^^^^^^^^^^^^

    return (
      <div>
        <button onClick={() => this.setState({ navigate: true })}>
          Home
        </button>
      </div>
    )
  }
}

完整示例 here。 阅读更多 here.

PS。如果您有兴趣,该示例也使用 ES7+ Property Initializers to initialise state. Look here

您也可以简单地使用道具来访问历史对象:this.props.history.push('new_url')

完成它的最简单方法:

this.props.history.push("/new/url")

注:

  • 您可能希望将 history prop 从父组件向下传递到您要在其不可用时调用操作的组件。

有时我更喜欢通过应用程序然后通过按钮切换路由,这是一个对我有用的最小工作示例:

import { Component } from 'react'
import { BrowserRouter as Router, Link } from 'react-router-dom'

class App extends Component {
  constructor(props) {
    super(props)

    /** @type BrowserRouter */
    this.router = undefined
  }

  async handleSignFormSubmit() {
    await magic()
    this.router.history.push('/')
  }

  render() {
    return (
      <Router ref={ el => this.router = el }>
        <Link to="/signin">Sign in</Link>
        <Route path="/signin" exact={true} render={() => (
          <SignPage onFormSubmit={ this.handleSignFormSubmit } />
        )} />
      </Router>
    )
  }
}

我的回答与类似。我不确定为什么 React-Router 使它变得如此不必要地复杂。为什么我必须用 HoC 包装我的组件才能访问本质上是全局的组件?

无论如何,如果你看一下他们是如何实施的<BrowserRouter>, it's just a tiny wrapper around history

我们可以提取该历史记录,以便我们可以从任何地方导入它。然而,诀窍在于,如果您正在执行服务器端呈现并尝试 import 历史模块,它将无法工作,因为它使用仅限浏览器的 API。但这没关系,因为我们通常只会在响应点击或其他一些客户端事件时进行重定向。因此,伪造它可能是可以的:

// history.js
if(__SERVER__) {
    module.exports = {};
} else {
    module.exports = require('history').createBrowserHistory();
}

在 webpack 的帮助下,我们可以定义一些变量,以便我们知道我们所处的环境:

plugins: [
    new DefinePlugin({
        '__SERVER__': 'false',
        '__BROWSER__': 'true', // you really only need one of these, but I like to have both
    }),

现在您可以

import history from './history';

来自任何地方。它只是 return 服务器上的一个空模块。

如果您不想使用这些魔术变量,则只需 require 在需要它的全局对象中(在您的事件处理程序中)。 import 不会工作,因为它只在顶层工作。

我为此苦苦挣扎了一段时间 - 如此简单却又如此复杂的东西,因为 ReactJS 只是一种完全不同的编写 Web 应用程序的方式,它对我们这些年长的人来说非常陌生!

我创建了一个单独的组件来抽象混乱:

// LinkButton.js

import React from "react";
import PropTypes from "prop-types";
import {Route} from 'react-router-dom';

export default class LinkButton extends React.Component {

    render() {
        return (
            <Route render={({history}) => (
                <button {...this.props}
                       onClick={() => {
                           history.push(this.props.to)
                       }}>
                    {this.props.children}
                </button>
            )}/>
        );
    }
}

LinkButton.propTypes = {
    to: PropTypes.string.isRequired
};

然后将其添加到您的 render() 方法中:

<LinkButton className="btn btn-primary" to="/location">
    Button Text
</LinkButton>

由于没有其他方法可以处理这种可怕的设计,我编写了一个使用 withRouter HOC 方法的通用组件。下面的示例包装了一个 button 元素,但您可以更改为您需要的任何可点击元素:

import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';

const NavButton = (props) => (
  <Button onClick={() => props.history.push(props.to)}>
    {props.children}
  </Button>
);

NavButton.propTypes = {
  history: PropTypes.shape({
    push: PropTypes.func.isRequired
  }),
  to: PropTypes.string.isRequired
};

export default withRouter(NavButton);

用法:

<NavButton to="/somewhere">Click me</NavButton>

第 1 步:只有一件事要在上面导入:

import {Route} from 'react-router-dom';

第 2 步:在您的路线中,传递历史记录:

<Route
  exact
  path='/posts/add'
  render={({history}) => (
    <PostAdd history={history} />
  )}
/>

第 3 步:history 在下一个组件中被接受为 props 的一部分,因此您可以简单地:

this.props.history.push('/');

这很简单,也很强大。

这个有效:

import { withRouter } from 'react-router-dom';

const SomeComponent = withRouter(({ history }) => (
    <div onClick={() => history.push('/path/some/where')}>
        some clickable element
    </div>); 
);

export default SomeComponent;

我认为@rgommezz 涵盖了大部分情况,减去我认为非常重要的情况。

// history is already a dependency or React Router, but if don't have it then try npm install save-dev history

import createHistory from "history/createBrowserHistory"

// in your function then call add the below 
const history = createHistory();
// Use push, replace, and go to navigate around.
history.push("/home");

这让我可以用 actions/calls 编写一个简单的服务,我可以调用它从我想要的任何组件进行导航,而无需在我的组件上做很多 HoC...

不清楚为什么以前没有人提供这个解决方案。希望对您有所帮助,如果您发现任何问题,请告诉我。

对于那些在使用 React RouterReact Router Dom 完全初始化路由器之前需要重定向的人,您可以通过简单地访问历史对象并将新状态推送到它上来提供重定向app.js 的构造函数。考虑以下因素:

function getSubdomain(hostname) {
    let regexParse = new RegExp('[a-z\-0-9]{2,63}\.[a-z\.]{2,5}$');
    let urlParts = regexParse.exec(hostname);
    return hostname.replace(urlParts[0], '').slice(0, -1);
}

class App extends Component {

    constructor(props) {
        super(props);


        this.state = {
            hostState: true
        };

        if (getSubdomain(window.location.hostname).length > 0) {
            this.state.hostState = false;
            window.history.pushState('', '', './login');
        } else {
            console.log(getSubdomain(window.location.hostname));
        }

    }


    render() {
        return (

            <BrowserRouter>
                {this.state.hostState ? (
                    <div>
                        <Route path="/login" component={LoginContainer}/>
                        <Route path="/" component={PublicContainer}/>
                    </div>
                ) : (
                    <div>
                        <Route path="/login" component={LoginContainer}/>
                    </div>
                )

                }
            </BrowserRouter>)
    }


}

这里我们想要更改依赖于子域的输出路由,通过在组件呈现之前与历史对象交互,我们可以有效地重定向,同时仍然保持我们的路由完整。

window.history.pushState('', '', './login');

如果您使用函数组件,请使用 useHistory 钩子

您可以使用 useHistory 钩子来获取 history 个实例。

import { useHistory } from "react-router-dom";

const MyComponent = () => {
  const history = useHistory();
  
  return (
    <button onClick={() => history.push("/about")}>
      Click me
    </button>
  );
}

useHistory 挂钩使您可以访问可用于导航的历史记录实例。

使用history属性内页组件

React Router 向页面组件注入一些属性,包括 history

class HomePage extends React.Component {
  render() {
    const { history } = this.props;

    return (
      <div>
        <button onClick={() => history.push("/projects")}>
          Projects
        </button>
      </div>
    );
  }
}

包装子组件withRouter以注入路由器属性

withRouter 包装器将路由器属性注入组件。例如,您可以使用此包装器将路由器注入用户菜单中的注销按钮组件。

import { withRouter } from "react-router";

const LogoutButton = withRouter(({ history }) => {
  return (
    <button onClick={() => history.push("/login")}>
      Logout
    </button>
  );
});

export default LogoutButton;
this.props.history.push("/url")

如果您在组件中找不到 this.props.history, 然后试试这个

import {withRouter} from 'react-router-dom'
export default withRouter(MyComponent)  

您可以通过这种方式有条件地导航

import { useHistory } from "react-router-dom";

function HomeButton() {
  const history = useHistory();

  function handleClick() {
    history.push("/path/some/where");
  }

  return (
    <button type="button" onClick={handleClick}>
      Go home
    </button>
  );
}