Can't navigate to other component - reactjs

i've got 2 components.
They are each on their own path.
I have a <Link> component in the first component, and when i click that, the second component is supposed to be rendered.
The url after the #, changes fine, but nothing happens in the ui.
This is the main component:
export default class IntegrationApp extends React.PureComponent {
render() {
return (
<>
<Route path="/details/:encryptedId/:integrationType/" component={DetailsOverview} />
<Route exact path="/:integrationSource?/" component={IntegrationsOverview} />
</>
);
}
}
ReactDOM.render(
<HashRouter>
<IntegrationApp />
</HashRouter>
, document.getElementById('integrationsOverviewContainer'));
The <Link> is a custom component.
It looks like this:
export default class LinkForRouting extends React.PureComponent {
render() {
let cssClass = this.props.isButton ? ' btn ' : '';
cssClass += this.props.isPrimary ? ' btn-primary ' : '';
cssClass += this.props.customCssClass ? this.props.customCssClass : '';
const clickEvent = this.props.handleClick ? () => this.props.handleClick() : ()=> function () { return null };
return (
<Link to={this.props.path} replace={true} className={cssClass} onClick={clickEvent}>
{this.props.children}
</Link>
);
}
}
The <Link> component is used inside the <IntegrationsOverview> component, like this:
<LinkForRouting path={`/details/${integration.encryptedId}/${integration.integrationType}`} isButton={false} isPrimary={false} > Vis </LinkForRouting>
If i click the link, and then hit F5 afterwards, then the <DetailsOverview> component renders fine, but if i just click the link, then the URL just changes, and nothing happens.
Any idea of how to trigger the <DetailsOverview> component, to trigger when i click on the <Link> component?

By reading the code posted above all of your components are extending PureComponent.
As React docs says:
React.PureComponent’s shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.Furthermore, React.PureComponent’s shouldComponentUpdate() skips prop updates for the whole component subtree. Make sure all the children components are also “pure”.
In your case:
export default class IntegrationApp extends React.PureComponent {
render() {
return (
<>
<Route path="/details/:encryptedId/:integrationType/" component={DetailsOverview} />
<Route exact path="/:integrationSource?/" component={IntegrationsOverview} />
</>
);
}
}
The IntegrationApp Component it is not expecting simple props and state also you should use a Switch instead of Fragment to wrap your Routes, then it becomes:
import React from "react";
import {
Switch,
Route
} from "react-router-dom";
export default class IntegrationApp extends React.Component {
render() {
return ( <
Switch >
<
Route path = "/details/:encryptedId/:integrationType/"
component = {
DetailsOverview
}
/> <
Route exact path = "/:integrationSource?/"
component = {
IntegrationsOverview
}
/> <
/Switch>
);
}
}
I wrote a simple example on: https://codesandbox.io/s/5vk79jlrn4
Hope it can help you.

The problem has been solved!
My react solution, was part of a much larger, and older solution.
This solution also had some references to angular, solution wide.
I made those references, specific to the views that need them, and then routing in react started working!
Lesson learned, check included javascript.

Related

Programmatically navigate using with React router v4 [duplicate]

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?
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.
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.
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.
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.
Use useHistory hook if you're using function components
You can use useHistory hook to get history instance.
import { useHistory } from "react-router-dom";
const MyComponent = () => {
const history = useHistory();
return (
<button onClick={() => history.push("/about")}>
Click me
</button>
);
}
The useHistory hook gives you access to the history instance that you may use to navigate.
Use history property inside page components
React Router injects some properties including history to page components.
class HomePage extends React.Component {
render() {
const { history } = this.props;
return (
<div>
<button onClick={() => history.push("/projects")}>
Projects
</button>
</div>
);
}
}
Wrap child components withRouter to inject router properties
withRouter wrapper injects router properties to components. For example you can use this wrapper to inject router to logout button component placed inside user menu.
import { withRouter } from "react-router";
const LogoutButton = withRouter(({ history }) => {
return (
<button onClick={() => history.push("/login")}>
Logout
</button>
);
});
export default LogoutButton;
You can also simply use props to access history object: this.props.history.push('new_url')
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.
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.
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.
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;
You can navigate conditionally by this way
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>
);
}
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.
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>
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>
this.props.history.push("/url")
If you have not found this.props.history available in your component ,
then try this
import {withRouter} from 'react-router-dom'
export default withRouter(MyComponent)
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>
)
}
}
For those of you who require to redirect before fully initalizing a router using React Router or React Router Dom You can provide a redirect by simply accesing the history object and pushing a new state onto it within your constructur of app.js. Consider the following:
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>)
}
}
Here we want to change the output Routes dependant on a subdomain, by interacting with the history object before the component renders we can effectively redirect while still leaving our routes in tact.
window.history.pushState('', '', './login');

Prevent remounting of expensive (class) component with shouldComponentUpdate() (List of images, inside router)

I'm rendering a very large list of images that, when clicked individually, are added to a "viewer" div. The problem is that each time I add an image to the viewer, the original list re-renders, even though no changes have been made to the list's content.
I've tried using shouldComponentUpdate() at every level, as well as using React.memo. Neither appear to have any effect. I've also looked in to whether the time should be spent making the components functional and researching hooks (useContext() looks enticing), but I'm too new at React to know if that would just be more time wasted. (Please feel free weigh in on whether this is a waste of time.)
I don't know where the problem is, so I'm not sure a snippet would do much good. Instead, I've stripped down the problem to its bones and posted a sandbox version here
https://codesandbox.io/s/async-darkness-l920b
At the moment, my shouldComponentUpdate comparison is pretty straightforward for each class; something like:
if (nextProps.photoData === this.props.photoData) {
return false;
} else {
return true;
}
If you open the codesandbox console you'll see I'm logging Year.js > <ImageList /> is rendering to flag each successive render of the list in question.
Any help, even a nudge in the right direction, would be hugely appreciated. I've been reading blog articles for a solid day now and nothing seems to help.
That's because the PhotoView in App.js is defined inside render method, so when state update causing the render, then the PhotoView redefined again. It's a new component every time for The App component.
Please define components outside the render function:
import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Year from "./Year";
import Viewer from "./Viewer";
import dataObj from "./dataObj.json";
import "./App.css";
class App extends React.Component {
constructor(props) {
super(props);
this.PhotoView = this.PhotoView.bind(this)
this.state = {
current: {
year: 2019,
url: ""
},
viewerData: [],
photoData: null
};
}
componentDidMount() {
this.setState({
photoData: dataObj
});
}
addToViewer = moment =>
this.setState(state => {
const viewerData = state.viewerData.concat(moment.props.data);
return {
viewerData,
value: ""
};
});
About() {
return (
<div>
<h1>About</h1>
</div>
);
};
PhotoView(url) {
return (
<div className="PhotoView">
<Year
setCurrent={this.setCurrent}
photoData={this.state.photoData}
addToViewer={this.addToViewer}
/>
<Viewer
viewerData={this.state.viewerData}
setCurrent={this.setCurrent}
/>
</div>
);
}
render() {
return (
<div className="App">
<Router>
<nav>
<Link to="/about">About</Link>
<Link to="/">Photo View</Link>
</nav>
<Switch>
<Route path="/about" exact component={this.About} />
<Route path="/" component={this.PhotoView} />
</Switch>
</Router>
</div>
);
}
}
export default App;
Or move them to individual files.

React Context API + withRouter - can we use them together?

I built a large application where a single button on the navbar opens a modal.
I'm keeping track of the modalOpen state using context API.
So, user clicks button on navbar. Modal Opens. Modal has container called QuoteCalculator.
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.props.history.push('/quote');
// ..
};
render() {
//...
return(<Question {...props} next={this.onSubmit} />;)
}
}
export default withRouter(QuoteCalculator);
Now, everything works as expected. When the user submits, I go to the right route. I just see the following warning on the console
index.js:1446 Warning: withRouter(QuoteCalculator): Function
components do not support contextType.
I'm tempted to ignore the warning, but I don't think its a good idea.
I tried using Redirect alternatively. So something like
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.setState({done: true});
// ..
};
render() {
let toDisplay;
if(this.state.done) {
toDisplay = <Redirect to="/quote"/>
} else {
toDipslay = <Question {...props} next={this.onSubmit} />;
}
return(<>{toDisplay}</>)
}
}
export default QuoteCalculator;
The problem with this approach is that I kept on getting the error
You tried to redirect to the same route you're currently on
Also, I'd rather not use this approach, just because then I'd have to undo the state done (otherwise when user clicks button again, done is true, and we'll just get redirected) ...
Any idea whats going on with withRouter and history.push?
Here's my app
class App extends Component {
render() {
return (
<Layout>
<Switch>
<Route path="/quote" component={Quote} />
<Route path="/pricing" component={Pricing} />
<Route path="/about" component={About} />
<Route path="/faq" component={FAQ} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
</Layout>
);
}
}
Source of the warning
Unlike most higher order components, withRouter is wrapping the component you pass inside a functional component instead of a class component. But it's still calling hoistStatics, which is taking your contextType static and moving it to the function component returned by withRouter. That should usually be fine, but you've found an instance where it's not. You can check the repo code for more details, but it's short so I'm just going to drop the relevant lines here for you:
function withRouter(Component) {
// this is a functional component
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<Route
children={routeComponentProps => (
<Component
{...remainingProps}
{...routeComponentProps}
ref={wrappedComponentRef}
/>
)}
/>
);
};
// ...
// hoistStatics moves statics from Component to C
return hoistStatics(C, Component);
}
It really shouldn't negatively impact anything. Your context will still work and will just be ignored on the wrapping component returned from withRouter. However, it's not difficult to alter things to remove that problem.
Possible Solutions
Simplest
Since all you need in your modal is history.push, you could just pass that as a prop from the modal's parent component. Given the setup you described, I'm guessing the modal is included in one place in the app, fairly high up in the component tree. If the component that includes your modal is already a Route component, then it has access to history and can just pass push along to the modal. If it's not, then wrap the parent component in withRouter to get access to the router props.
Not bad
You could also make your modal component a simple wrapper around your modal content/functionality, using the ModalContext.Consumer component to pass the needed context down as props instead of using contextType.
const Modal = () => (
<ModalContext.Consumer>
{value => <ModalContent {...value} />}
</ModalContext.Consumer>
)
class ModalContent extends React.Component {
onSubmit = () => {
// ...
this.props.toggleModal()
this.props.history.push('/quote')
// ..
}
// ...
}

How to manipulate history in React Router v4?

I am using:
React, Redux, React-Router, Meteor
Following situation:
I have a modal with two tabs, based on the chosen tab a different list of selections will be shown in the content area, picking a selection will then give a list of subselection (all within the modal).
Desired behavior:
I want the user to be able to use the browser's back button to "undo" clicking on one of the tabs or picking a selection.
What I tried so far with no success:
use withRouter HOC to wrap my component, so that I would get access to history. I would then use history.push(currentUrl,{selectedTab:selection})
-> Problem: the state from push wasn't stored on this histories location object, but only in the state of the history object associated with my rootcomponent (way further up the tree)
(More promising so far) Simply import the history I created with createHistory in another module and put that into component state on construction.
I would then access the push method by using this.state.history.push(currentUrl,{selectedTab:selection}), which works fine and I can find the pushed state under this.state.history.location.state.selectedTab. However, using the browser's back button does not cause a rerender and therefore the selection will stay the same.
Any ideas?
Cheers
Another approach you could take would be to use query params with React router. And in your component change the tabs based on the query param. Since the url changed you should get the default behaviour your looking for.
I have a project with almost exactly that situation. The difference, is that instead of it being in a modal, I have it in my header. Here is what I did:
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
class Header extends Component {
render() {
return (
<ul className="tabs tabs-fixed-width z-depth-1">
<li className="tab">
<Link id="books-tab" to={'/books'}>Books</Link>
</li>
<li className="tab">
<Link id="stats-tab" to={'/stats'}>Stats</Link>
</li>
{Meteor.userId() &&
<li className="tab">
<Link id="user-tab" to={'/user'}>Account Settings</Link>
</li>}
</ul>
);
}
}
export default withRouter(Header);
The react-router-dom <Link>... being the operative item that allows this to happen.
Then, when I click on the tab, each link goes to the specified url and keeps it in history so I can go back and get to the previous tab.
I refined the second approach and it is working now. It does still feel hacky to import the history into my module (as a local variable), and thus bypassing react tree, redux store and react router altogether.
However, it is working and not causing any bugs so far. So here we go ...
I instantiate my history object in: store.js module:
import createHistory from 'history/createBrowserHistory';
...
const history = createHistory();
...
export { history , store }
that as you can see exports history. I then feed said history to my Router (ConnectedRouter in my case, as I am using react-router-redux) in: routes.js module
import { store , history } from '/imports/client/store';
const MyStoreRouter = () => {
return (
<Provider store={store}>
<ConnectedRouter history={ history }>
<div>
<Switch>
<Route exact path="/" component={Landing}/>
<Route path="/" component={MainApp}/>
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
};
Now for the wacky part. I import the history into a display component and use it to set up a listener (to state changes in history.location) and change the state accordingly:
DisplayComponent.js module:
import { history } from '/imports/client/store';
....
export default class EventCreationModal extends Component {
constructor() {
super();
this.handleSelect = this.handleSelect.bind(this);
this.state = {
selectedCategory: (history.location.state && history.location.state.category) ? history.location.state.category : 'sports',
};
history.listen((location,action) => {
this.setState({
selectedCategory: (history.location.state && history.location.state.category) ? history.location.state.category : 'sports',
})
});
}
handleSelect(key) {
history.push("/Home",{'category':key})
}
render () {
return (
<Tabs activeKey={this.state.selectedCategory} onSelect={this.handleSelect} id="SportOrSocialSelector">
<Tab eventKey={'sports} .../>
<Tab eventKey={'social} .../>
</Tabs>
)
}

Programmatically navigate using react router V4

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?
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.
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.
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.
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.
Use useHistory hook if you're using function components
You can use useHistory hook to get history instance.
import { useHistory } from "react-router-dom";
const MyComponent = () => {
const history = useHistory();
return (
<button onClick={() => history.push("/about")}>
Click me
</button>
);
}
The useHistory hook gives you access to the history instance that you may use to navigate.
Use history property inside page components
React Router injects some properties including history to page components.
class HomePage extends React.Component {
render() {
const { history } = this.props;
return (
<div>
<button onClick={() => history.push("/projects")}>
Projects
</button>
</div>
);
}
}
Wrap child components withRouter to inject router properties
withRouter wrapper injects router properties to components. For example you can use this wrapper to inject router to logout button component placed inside user menu.
import { withRouter } from "react-router";
const LogoutButton = withRouter(({ history }) => {
return (
<button onClick={() => history.push("/login")}>
Logout
</button>
);
});
export default LogoutButton;
You can also simply use props to access history object: this.props.history.push('new_url')
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.
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.
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.
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;
You can navigate conditionally by this way
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>
);
}
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.
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>
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>
this.props.history.push("/url")
If you have not found this.props.history available in your component ,
then try this
import {withRouter} from 'react-router-dom'
export default withRouter(MyComponent)
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>
)
}
}
For those of you who require to redirect before fully initalizing a router using React Router or React Router Dom You can provide a redirect by simply accesing the history object and pushing a new state onto it within your constructur of app.js. Consider the following:
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>)
}
}
Here we want to change the output Routes dependant on a subdomain, by interacting with the history object before the component renders we can effectively redirect while still leaving our routes in tact.
window.history.pushState('', '', './login');

Resources