React Router V4 - Page does not rerender on changed Route - reactjs

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

Related

Building portfolio website and I want to include my github link [duplicate]

Since I'm using React Router to handle my routes in a React app, I'm curious if there is a way to redirect to an external resource.
Say someone hits:
example.com/privacy-policy
I would like it to redirect to:
example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies
I'm finding exactly zero help in avoiding writing it in plain JavaScript at my index.html loading with something like:
if (window.location.path === "privacy-policy"){
window.location = "example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies"
}
Here's a one-liner for using React Router to redirect to an external link:
<Route path='/privacy-policy' component={() => {
window.location.href = 'https://example.com/1234';
return null;
}}/>
It uses the React pure component concept to reduce the component's code to a single function that, instead of rendering anything, redirects browser to an external URL.
It works both on React Router 3 and 4.
With Link component of react-router you can do that. In the "to" prop you can specify 3 types of data:
a string: A string representation of the Link location, created by concatenating the location’s pathname, search, and hash properties.
an object: An object that can have any of the following properties:
pathname: A string representing the path to link to.
search: A string representation of query parameters.
hash: A hash to put in the URL, e.g. #a-hash.
state: State to persist to the location.
a function: A function to which current location is passed as an argument and which should return location representation as a string or as an object
For your example (external link):
https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies
You can do the following:
<Link to={{ pathname: "https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies" }} target="_blank" />
You can also pass props you’d like to be on the such as a title, id, className, etc.
There isn’t any need to use the <Link /> component from React Router.
If you want to go to external link use an anchor tag.
<a target="_blank" href="https://meetflo.zendesk.com/hc/en-us/articles/230425728-Privacy-Policies">Policies</a>
It doesn't need to request React Router. This action can be done natively and it is provided by the browser.
Just use window.location.
With React Hooks
const RedirectPage = () => {
React.useEffect(() => {
window.location.replace('https://www.google.com')
}, [])
}
With React Class Component
class RedirectPage extends React.Component {
componentDidMount(){
window.location.replace('https://www.google.com')
}
}
Also, if you want to open it in a new tab:
window.open('https://www.google.com', '_blank');
I actually ended up building my own Component, <Redirect>.
It takes information from the react-router element, so I can keep it in my routes. Such as:
<Route
path="/privacy-policy"
component={ Redirect }
loc="https://meetflo.zendesk.com/hc/en-us/articles/230425728-Privacy-Policies"
/>
Here is my component in case anyone is curious:
import React, { Component } from "react";
export class Redirect extends Component {
constructor( props ){
super();
this.state = { ...props };
}
componentWillMount(){
window.location = this.state.route.loc;
}
render(){
return (<section>Redirecting...</section>);
}
}
export default Redirect;
Note: This is with react-router: 3.0.5, it is not so simple in 4.x
I went through the same issue. I want my portfolio to redirect to social media handles. Earlier I used {Link} from "react-router-dom". That was redirecting to the sub directory as here,
Link can be used for routing web pages within a website. If we want to redirect to an external link then we should use an anchor tag. Like this,
Using some of the information here, I came up with the following component which you can use within your route declarations. It's compatible with React Router v4.
It's using TypeScript, but it should be fairly straightforward to convert to native JavaScript:
interface Props {
exact?: boolean;
link: string;
path: string;
sensitive?: boolean;
strict?: boolean;
}
const ExternalRedirect: React.FC<Props> = (props: Props) => {
const { link, ...routeProps } = props;
return (
<Route
{...routeProps}
render={() => {
window.location.replace(props.link);
return null;
}}
/>
);
};
And use with:
<ExternalRedirect
exact={true}
path={'/privacy-policy'}
link={'https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies'}
/>
The simplest solution is to use a render function and change the window.location.
<Route path="/goToGoogle"
render={() => window.location = "https://www.google.com"} />
If you want a small reusable component, you can just extract it like this:
const ExternalRedirect = ({ to, ...routeProps }) => {
return <Route {...routeProps} render={() => window.location = to} />;
};
and then use it (e.g. in your router switch) like this:
<Switch>
...
<ExternalRedirect exact path="/goToGoogle" to="https://www.google.com" />
</Switch>
I had luck with this:
<Route
path="/example"
component={() => {
global.window && (global.window.location.href = 'https://example.com');
return null;
}}
/>
I solved this on my own (in my web application) by adding an anchor tag and not using anything from React Router, just a plain anchor tag with a link as you can see in the picture screenshot of using anchor tag in a React app without using React Router
Basically, you are not routing your user to another page inside your app, so you must not use the internal router, but use a normal anchor.
Although this is for a non-react-native solution, but you can try.
In React Router v6, component is unavailable. Instead, now it supports element. Make a component redirecting to the external site and add it as shown.
import * as React from 'react';
import { Routes, Route } from "react-router-dom";
function App() {
return(
<Routes>
// Redirect
<Route path="/external-link" element={<External />} />
</Routes>
);
}
function External() {
window.location.href = 'https://google.com';
return null;
}
export default App;
In React Route V6 render props were removed. It should be a redirect component.
RedirectUrl:
const RedirectUrl = ({ url }) => {
useEffect(() => {
window.location.href = url;
}, [url]);
return <h5>Redirecting...</h5>;
};
Route:
<Routes>
<Route path="/redirect" element={<RedirectUrl url="https://google.com" />} />
</Routes>
I think the best solution is to just use a plain old <a> tag. Everything else seems convoluted. React Router is designed for navigation within single page applications, so using it for anything else doesn't make a whole lot of sense. Making an entire component for something that is already built into the <a> tag seems... silly?
To expand on Alan's answer, you can create a <Route/> that redirects all <Link/>'s with "to" attributes containing 'http:' or 'https:' to the correct external resource.
Below is a working example of this which can be placed directly into your <Router>.
<Route path={['/http:', '/https:']} component={props => {
window.location.replace(props.location.pathname.substr(1)) // substr(1) removes the preceding '/'
return null
}}/>
I don't think React Router provides this support. The documentation mentions
A < Redirect > sets up a redirect to another route in your application to maintain old URLs.
You could try using something like React-Redirect instead.
I was facing the same issue and solved it using by http:// or https:// in React.
Like as:
<a target="_blank" href="http://www.example.com/" title="example">See detail</a>
You can use for your dynamic URL:
<Link to={{pathname:`${link}`}}>View</Link>
For V3, although it may work for V4. Going off of Eric's answer, I needed to do a little more, like handle local development where 'http' is not present on the URL. I'm also redirecting to another application on the same server.
Added to the router file:
import RedirectOnServer from './components/RedirectOnServer';
<Route path="/somelocalpath"
component={RedirectOnServer}
target="/someexternaltargetstring like cnn.com"
/>
And the Component:
import React, { Component } from "react";
export class RedirectOnServer extends Component {
constructor(props) {
super();
// If the prefix is http or https, we add nothing
let prefix = window.location.host.startsWith("http") ? "" : "http://";
// Using host here, as I'm redirecting to another location on the same host
this.target = prefix + window.location.host + props.route.target;
}
componentDidMount() {
window.location.replace(this.target);
}
render(){
return (
<div>
<br />
<span>Redirecting to {this.target}</span>
</div>
);
}
}
export default RedirectOnServer;
I am offering an answer relevant to React Router v6 to handle dynamic routing.
I created a generic component called redirect:
export default function Redirect(params) {
window.location.replace('<Destination URL>' + "/." params.destination);
return (
<div />
)
}
I then called it in my router file:
<Route path='/wheretogo' element={<Redirect destination="wheretogo"/>}/>
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
function App() {
return (
<Router>
<Route path="/" exact>
{window.location.replace("http://agrosys.in")}
</Route>
</Router>
);
}
export default App;
Using React with TypeScript, you get an error as the function must return a React element, not void. So I did it this way using the Route render method (and using React router v4):
redirectToHomePage = (): null => {
window.location.reload();
return null;
};
<Route exact path={'/'} render={this.redirectToHomePage} />
Where you could instead also use window.location.assign(), window.location.replace(), etc.
Complementing Víctor Daniel's answer here: Link's pathname will actually take you to an external link only when there's the 'https://' or 'http://' before the link.
You can do the following:
<Link to={{ pathname:
> "https://example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies"
> }} target="_blank" />
Or if your URL doesn't come with 'https://', I'd do something like:
<Link to={{pathname:`https://${link}`}} target="_blank" />
Otherwise it will prepend the current base path, as Lorenzo Demattécommented.
If you are using server-side rending, you can use StaticRouter. With your context as props and then adding <Redirect path="/somewhere" /> component in your app. The idea is every time React Router matches a redirect component it will add something into the context you passed into the static router to let you know your path matches a redirect component.
Now that you know you hit a redirect you just need to check if that’s the redirect you are looking for. then just redirect through the server. ctx.redirect('https://example/com').
You can now link to an external site using React Link by providing an object to to with the pathname key:
<Link to={ { pathname: '//example.zendesk.com/hc/en-us/articles/123456789-Privacy-Policies' } } >
If you find that you need to use JavaScript to generate the link in a callback, you can use window.location.replace() or window.location.assign().
Over using window.location.replace(), as other good answers suggest, try using window.location.assign().
window.location.replace() will replace the location history without preserving the current page.
window.location.assign() will transition to the URL specified, but will save the previous page in the browser history, allowing proper back-button functionality.
location.replace()
location.assign()
Also, if you are using a window.location = url method as mentioned in other answers, I highly suggest switching to window.location.href = url.
There is a heavy argument about it, where many users seem to adamantly want to revert the newer object type window.location to its original implementation as string merely because they can (and they egregiously attack anyone who says otherwise), but you could theoretically interrupt other library functionality accessing the window.location object.
Check out this conversation. It's terrible.
JavaScript: Setting location.href versus location
I was able to achieve a redirect in react-router-dom using the following
<Route exact path="/" component={() => <Redirect to={{ pathname: '/YourRoute' }} />} />
For my case, I was looking for a way to redirect users whenever they visit the root URL http://myapp.com to somewhere else within the app http://myapp.com/newplace. so the above helped.

histroy.push() is not working in react router, if my base route is same

I have seen so many questions but haven't got the answer.
When i do history.push('/user') when i am on '/dashboard', it works great because my base route is different i.e user!=dashboard. But when i am on route '/user' and now i want to go to history.push('/user/87'). Here the weird thing happens as base route is same user===user, my url change but my component did not render again. I have use the <Link> thing which the react-router-dom provides but it doesn't work too.
here is my root thing,
import { BrowserRouter } from 'react-router-dom'; //"^5.0.1"
ReactDOM.render((
<Provider store={store}>
<BrowserRouter>
<AppContainer />
</BrowserRouter>
</Provider>
), document.getElementById('root'));
Please provide the solution it would be great help.
I found one trick with my colleague, we can use getDerivedStateFromProps and componentDidUpdate life cycles
state = {
currentUrl: this.props.match.url,
}
static getDerivedStateFromProps(props, state) {
let updatedState = {};
if (props.match.url !== state.currentUrl) {
updatedState.currentUrl = props.match.url;
}
return updatedState;
}
componentDidUpdate(prevProps) {
if (prevProps.match.url !== this.state.currentUrl) {
// re-initiate your state so that it can run your render lifecycle
this.setInitialState();
}
}
We can also use useEffect hook by providing [this.props.match.url] in second argument so that when ever your url updates you can set your state.
maybe do a this.forceUpdate() right after you push or use :params

How to detect route changes with react-router v4?

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 />;
}
};

How is react-router being created when using contextTypes?

I've got react-router 4.0.0-alpha.4 installed in my React project, which is a tutorial that I'm following, and I'm having a really difficult time understanding how it's being called in StorePicker.jsx
App.jsx
import React from 'react';
import { render } from 'react-dom';
import './css/style.css';
import {BrowserRouter, Match, Miss} from 'react-router';
import StorePicker from './components/StorePicker';
import App from './components/App';
import NotFound from './components/NotFound';
const Root = () => {
return(
<BrowserRouter>
<div>
<Match exactly pattern="/" component={StorePicker}/>
<Match pattern="/store/:storeId" component={App}/>
<Miss component={NotFound}/>
</div>
</BrowserRouter>
)
}
render(<Root/>, document.querySelector('#main'));
So towards the end in StorePicker.jsx I create a contextTypes property on the StorePicker object. I then set the value equal to { router: React.PropTypes.object }. React.PropTypes.object has nothing to do with the router though.
But in goToStore(event) all of a sudden the router object is available in this.context.router.
Where did this come from? Is it because the word router is a special keyword when I use it in contextTypes, and React.PropTypes.object somehow knows to fetch the router object as a result and add it to this.context?
Why is this tutorial even telling me to use this pattern? According to the React docs, context should be avoided: https://facebook.github.io/react/docs/context.html
Is there a better way to do this?
StorePicker.jsx
import React from 'react';
import { getFunName } from '../helpers.js'
class StorePicker extends React.Component{
goToStore(event){
event.preventDefault();
const storeId = "1234";
this.context.router.transitionTo(`/store/${storeId}`);
}
render(){
return (
<button onClick={(e) => this.goToStore(e)}>CLICK ME</button>
)
}
};
StorePicker.contextTypes = {
router: React.PropTypes.object
}
export default StorePicker;
The code from this tutorial works, but I have no idea why it's working.
3.Is there a better way to do this? :
You should not use context. It seems you only require to redirect to an url (whose value depends on the text box) when the form submits.
In React, you can use react routers.
Here's some more examples: StackOverflow: programmatically-navigate-using-react-router
Or in Javascript, if you just have to redirect to an url location you can simply use 'window.location' function as follows:
goToStore(event) {
event.preventDefault();
const storeId = this.storeInput.value;
window.location.assign(`/store/${storeId}`)
}
However, you may not want to use second option as Javascript renders whole dom and then there's no advantage of using React then.

How do I go to a specific page in React based on a URL param

As an example when entering http://localhost:3000/ui/?goto=/ui/entity/e2 in the browser I'd like to go to the Entity component e2.
This is my router:
<Route path="/ui/" component={App}>
<IndexRoute component={EntitiesPage} />
<Route component={Entity} path="entity/:id" />
<Route component={NotFound} path="*" />
</Route>
This is the App component:
import React from 'react'
const App = React.createClass( {
render() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
// go here: gotoUrl;
} else {
return (
<div className="App">
{this.props.children}
</div>
)
}
}
})
export default App
this.context is empty.
this.props has:
history
location
route
routeParams (empty)
routes
UPDATE:
I've ended up using this:
import React from 'react'
import { withRouter } from 'react-router'
const App = React.createClass( {
componentWillMount() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
this.props.router.replace(gotoUrl);
}
},
render() {
return (
<div className="App">
{this.props.children}
</div>
);
}
})
export default withRouter(App)
One thing that might be tripping you up is that render should have no side effects.
A "side effect" is anything that changes what's going on in your app*: updating state, making AJAX calls, or in this case, altering the page location. The render method should only read from the current state of the component, then return a value.
Because you're already using React.createClass, the best way to handle this is by adding a separate method that React handles specially: componentWillMount. I'd recommend you put your "redirect" logic here.
In order to properly change the page location, you'll need access to the browser history object which react-router manipulates. You can import this from the react-router library itself and directly call methods on it:
// At top of file
import { browserHistory } from 'react-router'
// Then, in your component:
componentWillMount() {
let gotoUrl = this.props.location.query.goto;
if (gotoUrl) {
// NOTE: this may have security implications; see below
browserHistory.push(gotoUrl);
}
}
Source: documentation.
I'd suggest that, instead of using query.goto, you instead select a parameter that can be easily validated, such as the entity ID itself (a simple regex can make sure it's valid). Otherwise, an unscrupulous user might send a link to another user and cause them to access a page that they didn't mean to.
*Note: there are stricter definitions of "side effect" out there, but this one is pretty useful for React development.
You should use browserHistory
import { browserHistory } from 'react-router';
...
if (gotoUrl) {
browserHistory.push(gotoUrl)
}
Let me know if this works

Resources