可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have just replaced react-router
from v3 to v4.
But I am not sure how to programmatically navigate in the member function of a Component
.
i.e in handleClick()
function I want to navigate to /path/some/where
after processing some data.
I used to do that by:
import { browserHistory } from \'react-router\'
browserHistory.push(\'/path/some/where\')
But I can\'t find such interfaces in v4.
How can I navigate using v4?
回答1:
If you are targeting browser environments, you need to use react-router-dom
package, instead of react-router
. They are following the same approach as React did, in order to separate the core, (react
) and the platform specific code, (react-dom
, react-native
) with the subtle difference that you don\'t need to install two separate packages, so the environment packages contain everything you need. You can add it to your project as:
yarn add react-router-dom
or
npm i react-router-dom
The first thing you need to do is to provide a <BrowserRouter>
as the top most parent component in your application. <BrowserRouter>
uses the HTML5 history
API and manages it for you, so you don\'t have to worry about instantiating it yourself and passing it down to the <BrowserRouter>
component as a prop (as you needed to do in previous versions).
In V4, for navigating programatically you need to access the history
object, which is available through React context
, as long as you have a <BrowserRouter>
provider component as the top most parent in your application. The library exposes through context the router
object, that itself contains history
as a property. The history
interface offers several navigation methods, such as push
, replace
and goBack
, among others. You can check the whole list of properties and methods here.
Important Note to Redux/Mobx users
If you are using redux or mobx as your state management library in your application, you may have come across issues with components that should be location-aware but are not re-rendered after triggering an URL update
That\'s happening because react-router
passes location
to components using the context model.
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.
The 2 approaches for solving this are:
- Wrap your connected component in a pathless
<Route />
. The current location
object is one of the props that a <Route>
passes to the component it renders
- Wrap your connected component with the
withRouter
higher-order component, that in fact has the same effect and injects location
as a prop
Setting that aside, there are four ways to navigate programatically, ordered by recommendation:
1.- Using a <Route>
Component
It promotes a declarative style. Prior to v4, <Route />
components were placed at the top of your component hierarchy, having to think of your routes structure beforehand. However, now you can have <Route>
components anywhere in your tree, allowing you to have a finer control for conditionally rendering depending on the URL. Route
injects match
, location
and history
as props into your component. The navigation methods (such as push
, replace
, goBack
...) are available as properties of the history
object.
There are 3 ways to render something with a Route
, by using either component
, render
or children
props, but don\'t use more than one in the same Route
. The choice depends on the use case, but basically the first two options will only render your component if the path
matches the url location, whereas with children
the component will be rendered whether the path matches the location or not (useful for adjusting the UI based on URL matching).
If you want to customise your component rendering output, you need to wrap your component in a function and use the render
option, in order to pass to your component any other props you desire, apart from match
, location
and history
. An example to illustrate:
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.- Using withRouter
HoC
This higher order component will inject the same props as Route
. However, it carries along the limitation that you can have only 1 HoC per file.
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.- Using a Redirect
component
Rendering a
<Redirect>
will navigate to a new location. But keep in mind that,
by default, the current location is replaced by the new one, like server-side redirects (HTTP 3xx). The new location is provided by
to
prop, that can be a string (URL to redirect to) or a
location
object. If you want to
push a new entry onto the history instead, pass a
push
prop as well and set it to
true
<Redirect to=\"/your-new-location\" push />
4.- Accessing router
manually through context
A bit discouraged because context is still an experimental API and it is likely to break/change in future releases of 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,
}),
};
Needless to say there are also other Router components that are meant to be for non browser ecosystems, such as <NativeRouter>
that replicates a navigation stack in memory and targets React Native platform, available through react-router-native
package.
For any further reference, don\'t hesitate to take a look at the official docs. There is also a video made by one of the co-authors of the library that provides a pretty cool introduction to react-router v4, highlighting some of the major changes.
回答2:
The easiest way to get it done:
this.props.history.push(\"/new/url\")
Note:
- You may want to pass the
history
prop
from parent component down to the component you want to invoke the action if its not available.
回答3:
I had a similar issue when migrating over to React-Router v4 so I\'ll try to explain my solution below.
Please do not consider this answer as the right way to solve the problem, I imagine there\'s a good chance something better will arise as React Router v4 becomes more mature and leaves beta (It may even already exist and I just didn\'t discover it).
For context, I had this problem because I occasionally use Redux-Saga
to programmatically change the history object (say when a user successfully authenticates).
In the React Router docs, take a look at the <Router>
component and you can see you have the ability to pass your own history object via a prop. This is the essence of the solution - we supply the history object to React-Router
from a global module.
Steps:
- Install the history npm module -
yarn add history
or npm install history --save
create a file called history.js
in your App.js
level folder (this was my preference)
// src/history.js
import createHistory from \'history/createBrowserHistory\';
export default createHistory();`
Add this history object to your Router component like so
// src/App.js
import history from \'../your/path/to/history.js;\'
<Router history={history}>
// Route tags here
</Router>
Adjust the URL just like before by importing your global history object:
import history from \'../your/path/to/history.js;\'
history.push(\'new/path/here/\');
Everything should stay synced up now, and you also have access to a way of setting the history object programmatically and not via a component/container.
回答4:
TL;DR:
if (navigate) {
return <Redirect to=\"/\" push={true} />
}
The simple and declarative answer is that you need to use <Redirect to={URL} push={boolean} />
in combination with 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>
)
}
}
Full example here.
Read more here.
PS. The example uses ES7+ Property Initializers to initialise state. Look here as well, if you\'re interested.
回答5:
Step 1: There is only one thing to import on top:
import {Route} from \'react-router-dom\';
Step 2: In your Route, pass the history:
<Route exact path=\'/posts/add\' render={({history}) => (
<PostAdd
history={history}
/>
.)}/>
Step 3: history gets accepted as part of props in the next Component, so you can simply:
this.props.history.push(\'/\');
That was easy and really powerful.
回答6:
This works:
import { withRouter } from \'react-router-dom\';
const SomeComponent = withRouter(({ history }) => (
<div onClick={() => history.push(\'/path/some/where\')}>
some clickable element
</div>);
);
export default SomeComponent;
回答7:
can also simply use props: this.props.history.push(\'new_url\')
回答8:
My answer is similar to Alex\'s. I\'m not sure why React-Router made this so needlessly complicated. Why should I have to wrap my component with a HoC just to get access to what\'s essentially a global?
Anyway, if you take a look at how they implemented <BrowserRouter>
, it\'s just a tiny wrapper around history.
We can pull that history bit out so that we can import it from anywhere. The trick, however, is if you\'re doing server-side rendering and you try to import
the history module, it won\'t work because it uses browser-only APIs. But that\'s OK because we usually only redirect in response to a click or some other client-side event. Thus it\'s probably OK to fake it:
// history.js
if(__SERVER__) {
module.exports = {};
} else {
module.exports = require(\'history\').createBrowserHistory();
}
With the help of webpack, we can define some vars so we know what environment we\'re in:
plugins: [
new DefinePlugin({
\'__SERVER__\': \'false\',
\'__BROWSER__\': \'true\', // you really only need one of these, but I like to have both
}),
And now you can
import history from \'./history\';
From anywhere. It\'ll just return an empty module on the server.
If you don\'t want use these magic vars, you\'ll just have to require
in the global object where it\'s needed (inside your event handler). import
won\'t work because it only works at the top-level.
回答9:
I\'ve been testing v4 for a few days now and .. I\'m loving it so far! It just makes sense after a while.
I also had the same question and I found handling it like the following worked best (and might even be how it is intended). It uses state, a ternary operator and <Redirect>
.
In the constructor()
this.state = {
redirectTo: null
}
this.clickhandler = this.clickhandler.bind(this);
In the render()
render(){
return (
<div>
{ this.state.redirectTo ?
<Redirect to={{ pathname: this.state.redirectTo }} /> :
(
<div>
..
<button onClick={ this.clickhandler } />
..
</div>
)
}
In the clickhandler()
this.setState({ redirectTo: \'/path/some/where\' });
Hope it helps. Let me know.
回答10:
Since there\'s no other way to deal with this horrible design, I wrote a generic component that uses the withRouter
HOC approach. The example below is wrapping a button
element, but you can change to any clickable element you need:
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);
Usage:
<NavButton to=\"/somewhere\">Click me</NavButton>
回答11:
I struggled with this for a while - something so simple, yet so complicated, because ReactJS is just a completely different way of writing web applications, it\'s very alien to us older folk!
I created a separate component to abstract the mess away:
// 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
};
Then add it to your render()
method:
<LinkButton className=\"btn btn-primary\" to=\"/location\">
Button Text
</LinkButton>
回答12:
As sometimes I prefer to switch routes by Application then by buttons, this is a minimal working example what works for me:
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>
)
}
}
回答13:
I think that @rgommezz covers most of the cases minus one that I think it\'s quite important.
// 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\");
This allows me to write a simple service with actions/calls that I can call to do the navigation from any component I want without doing a lot HoC on my components...
It is not clear why nobody has provided this solution before. I hope it helps, and if you see any issue with it please let me know.