How to detect route changes with react-router v4? - reactjs

I need to detect if a route change has occurred so that I can change a variable to true.
I've looked through these questions:
1. https://github.com/ReactTraining/react-router/issues/3554
2. How to listen to route changes in react router v4?
3. Detect Route Change with react-router
None of them have worked for me. Is there a clear way to call a function when a route change occurs.

One way is to use the withRouter higher-order component.
Live demo (click the hyperlinks to change routes and view the results in the displayed console)
You can get access to the history object's properties and the closest 's match via the withRouter higher-order component. withRouter will pass updated match, location, and history props to the wrapped component whenever it renders.
https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md
import {withRouter} from 'react-router-dom';
class App extends Component {
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
console.log('Route change!');
}
}
render() {
return (
<div className="App">
...routes
</div>
);
}
}
export default withRouter(props => <App {...props}/>);
Another example that uses url params:
If you were changing profile routes from /profile/20 to /profile/32
And your route was defined as /profile/:userId
componentDidUpdate(prevProps) {
if (this.props.match.params.userId !== prevProps.match.params.userId) {
console.log('Route change!');
}
}

With React Hooks, it should be as simple as:
useEffect(() => {
const { pathname } = location;
console.log('New path:', pathname);
}, [location.pathname]);
By passing location.pathname in the second array argument, means you are saying to useEffect to only re-run if location.pathname changes.
Live example with code source: https://codesandbox.io/s/detect-route-path-changes-with-react-hooks-dt16i

React Router v5 now detects the route changes automatically thanks to hooks. Here's the example from the team behind it:
import { Switch, useLocation } from 'react-router'
function usePageViews() {
let location = useLocation()
useEffect(
() => {
ga.send(['pageview', location.pathname])
},
[location]
)
}
function App() {
usePageViews()
return <Switch>{/* your routes here */}</Switch>
}
This example sends a "page view" to Google Analytics (ga) every time the URL changes.

When component is specified as <Route>'s component property, React Router 4 (RR4) passes to it few additional properties: match, location and history.
Then u should use componentDidUpdate lifecycle method to compare location objects before and after update (remember ES object comparison rules). Since location objects are immutable, they will never match. Even if u navigate to the same location.
componentDidUpdate(newProps) {
if (this.props.location !== newProps.location) {
this.handleNavigation();
}
}
withRouter should be used when you need to access these properties within an arbitrary component that is not specified as a component property of any Route. Make sure to wrap your app in <BrowserRouter> since it provides all the necessary API, otherwise these methods will only work in components contained within <BrowserRouter>.
There are cases when user decides to reload the page via navigation buttons instead of dedicated interface in browsers. But comparisons like this:
this.props.location.pathname !== prevProps.location.pathname
will make it impossible.

How about tracking the length of the history object in your application state? The history object provided by react-router increases in length each time a new route is traversed. See image below.
ComponentDidMount and ComponentWillUnMount check:

React use Component-Based Architecture. So, why don't we obey this rule?
You can see DEMO.
Each page must be wrapped by an HOC, this will detect changing of page automatically.
Home
import React from "react";
import { NavLink } from "react-router-dom";
import withBase from "./withBase";
const Home = () => (
<div>
<p>Welcome Home!!!</p>
<NavLink to="/login">Go to login page</NavLink>
</div>
);
export default withBase(Home);
withBase HOC
import React from "react";
export default WrappedComponent =>
class extends React.Component {
componentDidMount() {
this.props.handleChangePage();
}
render() {
return <WrappedComponent />;
}
};

Related

How to use react-router inside of a method

I'm new to react and react router dom(and javascript in general), and I've been racking my brain and googling all over the place but I can't figure out how to redirect someone in a method like exampleMethod() {}. I then call said method in an onClick function on a button. Here's my code for the button:
<Button color="secondary" onClick={this.rejoinPrevRoom}>
Rejoin previous room
</Button>
The button is inside a method called renderHomepage which is then called if there is a room code in the state inside the main render function.
The code for rejoinPrevRoom:
rejoinPrevRoom() {
console.log("Debug: A");
console.log(this.state.roomCode);
return <Redirect to={`/room/${this.state.roomCode}`} />;
}
It successfully logs a code so there is one but I don't know why it doesn't redirect them.
Any help would be appreciated greatly
Thank you,
Paddy
You cannot return JSX from a callback handler and expect it to have any effect on what the component is returning to render to the DOM. In this case you can either set some state to conditionally render a Redirect component as part of the render return, or use the history object to issue an imperative redirect via history.replace (not history.push).
rejoinPrevRoom() {
console.log("Debug: A");
console.log(this.state.roomCode);
this.props.history.replace(`/room/${this.state.roomCode}`);
}
If this component is not rendered directly by a Route component via the component, render, or children prop, then you can decorate it with the withRouter Higher Order Component in order to have the route props injected as props and have the history object available as this.props.history.
Update
In order to inject the route props change your HomePage export from
export default class HomePage extends Component {
....
}
to
class HomePage extends Component {
....
}
default export withRouter(HomePage);
If you're using React hooks you can redirect like the following:
const history = useHistory();
rejoinPrevRoom() {
console.log("Debug: A");
console.log(this.state.roomCode);
history.push(`/room/${this.state.roomCode}`);
}
If you're using class components:
rejoinPrevRoom() {
console.log("Debug: A");
console.log(this.state.roomCode);
const {history} = this.props;
history.push(`/room/${this.state.roomCode}`);
}
If the component is inside a BrowserRouter, you can use history to do that.
rejoinPrevRoom() {
console.log("Debug: A");
console.log(this.state.roomCode);
this.props.history.replace(`/room/${this.state.roomCode}`);
}

NEXT JS - How to prevent layout get re-mounted?

Trying next with layout pattern:
https://github.com/zeit/next.js/tree/canary/examples/layout-component
And the problem is that Layout component get remounted on every page change. I need to use layout component as a Container so it'll fetch data from server on every mount. How can I prevent layout to get re-mounted? Or am I missing something there?
This helped me for persistent layouts. The author puts together a function that wraps your page components in your Layout component and then passes that fetch function to your _app.js. This way the _app.js is actually the components that renders the Layout but you get to specify which pages use which layout (in case you have multiple layouts).
So you have the flexibility of having multiple layouts throughout your site but those pages that share the same layout will actually share the same layout component and it will not have to be remounted on navigation.
Here is the link to the full article
Persistent Layout Patterns in Next.js
Here are the important code snippets. A page and then _app.js
// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'
const AccountSettingsBasicInformation = () => (
<div>{/* ... */}</div>
)
AccountSettingsBasicInformation.getLayout = page => (
<SiteLayout>
<AccountSettingsLayout>{page}</AccountSettingsLayout>
</SiteLayout>
)
export default AccountSettingsBasicInformation
// /pages/_app.js
import React from 'react'
import App from 'next/app'
class MyApp extends App {
render() {
const { Component, pageProps, router } = this.props
const getLayout = Component.getLayout || (page => page)
return getLayout(<Component {...pageProps}></Component>)
}
}
export default MyApp
If you put your Layout component inside page component it will be re-remounted on page navigation (page switch).
You can wrap your page component with your Layout component inside _app.js, it should prevent it from re-mounting.
Something like this:
// _app.js
import Layout from '../components/Layout';
class MyApp extends App {
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
return {
...appProps,
};
}
render() {
const { Component, pageProps } = this.props;
return (
<Layout>
<Component {...pageProps} />
<Layout />
);
}
}
export default MyApp;
Also, make sure you replace all the to <Link href=""></Link>, notice that only have change the Html tag to link.
I struggled because with this for many days, although I was doing everything else correctly, these <a> tags were the culprit that was causing the _app.js remount on page change
Even though this is the topic Layout being mounted again and again, the root cause of this problem is that you have some data loaded in some child component which is getting fetched again and again.
After some fooling around, I found none of these problem is actually what Next.Js or SWR solves. The question, back to square one, is how to streamline a single copy of data to some child component.
Context
Use context as a example.
Config.js
import { createContext } from 'react'
export default createContext({})
_App.js
import Config from '../Config'
export default function App({ Component, pageProps }) {
return (
<Config.Provider value={{ user: { name: 'John' }}}>
<Component {...pageProps} />
</Config.Provider>
)
}
Avatar.js
import { useContext } from 'react'
import Config from '../Config'
function Avatar() {
const { user } = useContext(Config)
return (
<span>
{user.name}
</span>
)
}
export default Avatar
No matter how you mount and dismount, you won't end up with re-render, as long as the _app doesn't.
Writable
The above example is only dealing with readable. If it's writable, you can try to pass a state into context. setUser will take care the set in consumer.
<Provider value={useState({})} />
const [user, setUser] = useContext(Config)
setUser is "cached" and won't be updated. So we can use this function to reset the user anytime in child consumer.
There're other ways, ex. React Recoil. But more or less you are dealing with a state management system to send a copy (either value or function) to somewhere else without touching other nodes. I'll leave this as an answer, since even we solved Layout issue, this problem won't disappear. And if we solve this problem, we don't need to deal with Layout at all.

How to access current route from anywhere in my app?

I am trying to get information about the current route on a non-Route component.
React Router provides singleton versions of history (browserHistory
and hashHistory) that you can import and use from anywhere in your
application.
Previously, it would seem you could use browserHistory, but that no longer appears to be supported. I'm using react-router-redux^4.0.8 and #types\react-router-redux^5.0.1.
How can I access the current route location?
This is very simple. You can use the withRouter HOC
You can get access to the history object's properties and the closest
Route's match via the withRouter higher-order component. withRouter
will pass updated match, location, and history props to the wrapped
component whenever it renders.
It is used liked this:
import {withRouter} from "react-router-dom"
#withRouter
class NonRouteComponent extends Component {
render() {
// you have access to this.props.match
// you have access to this.props.history
// you have access to this.props.location
const { match, location, history } = this.props
return <h1>Hi {location.pathname}</h1>;
}
}
If you don't use decorators aka #withRouter you can export the component like so:
class NonRouteComponent extends Component { ... }
export default withRouter(NonRouteComponent);

React Router V4 - Page does not rerender on changed Route

I am building a small App to learn React with Meteor.
The user is asked to input a location, based on this location the user gets routed to a new page, where the location is displayed on the top of the page and some data from the database based on that location (not yet, this is my next step).
I store the location in state on the component. Now it would be nice if the user could change the location.
This is easy: just update state, e voilá. Or so I thought. The problem now is that the URL doesn't update, which looks dumb.
I then thought: "Okay, lets check on click if old location (in state) and new location (from input) are different, and if so set a redirect state"
In my render function if redirect is set I would just route the user to the same page again, but now the page won't reload. Any Ideas?
I know there are a thousand questions on react router v4 out there right now, because they just updated the version recently. I have been reading documentation for the last hours and just can't wrap my head around it.
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router'
import LocationPicker from './LocationPicker'
export default class LocationDisplay extends Component {
constructor () {
super();
this.setLocation = this.setLocation.bind(this);
this.state = {redirect: false};
}
setLocation(value) {
this.setState({locationId: value});
if (this.state.locationId != value) {
this.setState({redirect : true})
};
}
componentWillMount() {
this.setState({
locationId: this.props.match.params.locationId,
});
}
render () {
return (
this.state.redirect
?
<Redirect push to={{
pathname: `/location/${this.state.locationId}`
}}/>
:
<div>
<LocationPicker
returnLocation={this.setLocation}
locationId={this.state.locationId}
/>
<h1>Please find below all recommendations for {this.state.locationId}</h1>
</div>
)
}
}
And here my Routes:
import React from 'react';
import {
BrowserRouter as Router,
Route,
Link
} from 'react-router-dom';
import { render } from 'react-dom';
import App from './App';
import LocationPicker from './LocationPicker';
import LocationDisplay from './LocationDisplay';
Meteor.startup(() => {
render(
<Router>
<div>
<Route exact path="/" component={LocationPicker}/>
<Route path="/location/:locationId" component={LocationDisplay}/>
</div>
</Router>,
document.getElementById('render-target')
);
});
Update:
I tried using <Link> instead of <Redirect>, however this has the same effect. Even more so, it does not update URL at all now.
render () {
return (
this.state.redirect
?
<Link to={{
pathname: `/location/${this.state.locationId}`
}}/>
:
<div>
<LocationPicker
returnLocation={this.setLocation}
locationId={this.state.locationId}
/>
<h1>Please find bellow all recommendations for {this.state.locationId}</h1>
</div>
)
}
Any explanation regarding the problem in basic language would also be extremely appreciated. I am just not there yet :(
Cheers and thx alot
You can't expect to change the route just by changing the state. Even with the <Redirect> approach that you have used, it only make an infinite redirect loop by setting this.state.redirect true. Because Route use same LocationDisplay instance with each redirect and this.state.redirect will be always true. However, react-router automatically directs this redirect loop and renders a blank.
The correct way to change routes in react-router is use push method in history object. You can simply call push method with your new path name as follows.
setLocation(value) {
this.props.history.push(`/location/${value}`);
}
Also, I don't understand why you keep locationId in your state. It's already in your props at this.props.match.params.locationId. So there is no point of keep same data in two places because having a single source of truth is ideal. Otherwise, you always have to write additional code lines to keep locationId in state and props sync. So I suggest you to, change your approach to something like this.
export default class LocationDisplay extends Component {
constructor () {
super();
}
setLocation(value) {
this.props.history.push(`/location/${value}`);
}
render () {
const locationId = this.props.match.params.locationId
return (
<div>
<LocationPicker
returnLocation={this.setLocation}
locationId={locationId}
/>
<h1>Please find below all recommendations for {locationId}</h1>
</div>
)
}
}
(This code is just to get an idea. I didn't test this code as I don't have other parts. If you can provide codepen or jsfiddle with your current implementation I can update it to work.)
For those experiencing this problem with Redux, wrap your connect within withRouter, as shown here: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/blocked-updates.md

Automatic redirect after login with react-router

I wanted to build a Facebook login into my react/react-router/flux application.
I have a listener registered on the login event and would like to redirect the user to '/dashboard' if they are logged in. How can I do that? location.push didn't work very well, except after reloading the page completely.
React Router v3
This is what I do
var Router = require('react-router');
Router.browserHistory.push('/somepath');
React Router v4
Now we can use the <Redirect>component in React Router v4.
Rendering a <Redirect> will navigate to a new location. The new location will override the current location in the history stack, like server-side redirects.
import React, { Component } from 'react';
import { Redirect } from 'react-router';
export default class LoginComponent extends Component {
render(){
if(this.state.isLoggedIn === true){
return (<Redirect to="/your/redirect/page" />);
}else{
return (<div>Login Please</div>);
}
}
}
Documentation https://reacttraining.com/react-router/web/api/Redirect
React Router v0.13
The Router instance returned from Router.create can be passed around (or, if inside a React component, you can get it from the context object), and contains methods like transitionTo that you can use to transition to a new route.
React Router v2
Even though the question is already answered, I think it's relevant to post the solution that worked for me, since it wasn't covered in any of the solutions given here.
First, I'm using the router context on my LoginForm component
LoginForm.contextTypes = {
router: React.PropTypes.object
};
After that, I can access the router object inside my LoginForm component
handleLogin() {
this.context.router.push('/anotherroute');
}
PS: working on React-router version 2.6.0
React Router v3
Navigating Outside of Components
create your app with Router like this
// Your main file that renders a <Router>:
import { Router, browserHistory } from 'react-router'
import routes from './app/routes'
render(
<Router history={browserHistory} routes={routes} />,
mountNode
)
Somewhere like a Redux middleware or Flux action:
import { browserHistory } from 'react-router'
// Go to /some/path.
browserHistory.push('/some/path')
// Go back to previous location.
browserHistory.goBack()
react-router/tree/v3/docs
React Router v4.2.0
I am using React-16.2.0 & React-router-4.2.0
And I get solution by this code
this.props.history.push("/");
My working code:
.then(response => response.json())
.then(data => {
if(data.status == 200){
this.props.history.push("/");
console.log('Successfully Login');
}
})
I was following this document redirect-on-login-and-logout
I was also try by return <Redirect to='/' /> But unlucky, this not working for me.
React router v5 using hooks
These steps are for authorisation redirect. But can be used for login/logout redirection also.
The <Redirect/> accepts to prop as a string or an object. We can utilise the object to pass the redirection path after login/logout using hooks easily.
Get the pathname of url from where the <Redirect/> is called using
useLocation()
const {pathname} = useLocation()
In the to prop of <Redirect/> pass in the following object:
<Redirect to={{pathname:'/login',state: {referrer: pathname}}/>
In the Login component access the route state variable using useLocation() hook and use the useHistory() hook to redirect after successful login.
const history = useHistory();
const location = useLocation();
const login() => {
// After login success
const {state: {referrer}} = location;
history.push(referrer)
};
Check the official docs here
React Router v3
Navigating inside components
You should use withRouter decorator when it's necessary to redirect inside a component. The decorator uses context instead of you.
import {withRouter} from 'react-router'
fucntion Foo(props) {
props.router.push('/users/16');
}
export default withRouter(Foo);
withRouter(Component, [options])
A HoC (higher-order component) that wraps another component to enhance
its props with router props.
withRouterProps = {
...componentProps,
router,
params,
location,
routes
}
Pass in your component and it will return the
wrapped component.
You can explicit specify router as a prop to the wrapper component to
override the router object from context.
In your store:
data.router.transitionTo('user');
And router has:
"Route name="user" handler={User}"
User is route handler

Resources