React Router re-render route instead of re-render component - javascript

I'm working on an app that is using React Router and I noticed that when my Redux store changes state that the router is re-rendering the component the current route refers to rather than re-rendering the route itself.
To illustrate the problem; I have implemented a PrivateRoute that checks if a user is currently logged in. In it's most basic form it looks something like this:
const PrivateRoute = ({component: Component, ...rest}) => {
return <Route {...rest} render={(props) => {
const state = store.getState()
if (state.isAuthenticated) {
return <Component {...props}/>
}
else {
return <Redirect to={{pathname: '/login'}}/
}
}}/>
})
This works great since I can now say something like this:
<PrivateRoute path="/" component={HomePage}/>
However, I noticed that when the state of isAuthenticated changes that React Router is calling the render method on the HomePage component rather than that it re-renders the route. This means that the application will only do the authentication check when a user goes from some page to the home page but once on the home page, the check is no longer performed.
The only work around I have at the moment is to move the authentication check into the render function of the component (which is obviously not where it belongs).
How can I make React Router re-render the route rather than re-render the component the route refers to when the state changes?

I managed to solve the problem by using a Higher Order Component rather than implementing the authentication check in the route.
function withEnsureAuthentication(WrappedComponent) {
return class extends React.Component {
render() {
if (this.props.store.isAuthenticated === false) {
return <Redirect to={{pathname: '/login'}}/>
}
return <WrappedComponent {...this.props}/>
}
}
}
You can now use a normal Route but apply the withEnsureAuthentication to the component:
const HomePage = withEnsureAuthentication(Home)
<Route path="/home" component={HomePage}/>

Related

reactjs props getting lost in Route creation

I'm attempting to pass a user's auth state down to components via a react-router-dom switch block (I believe I ought to look at implementing redux but that's a question for later).
There's a Home view that gets passed all the login information after the user authenticates, and I can see the authState object as a prop in the home component using react devtools:
import React from "react";
import Dashboard from "../Dashboard";
import {Switch, Route} from "react-router-dom";
import NoMatch from "../NoMatch";
function Home(props) {
// authState exists
return (
<Switch {...props}>
// authState exists
<Route exact path="/" render={(...props) => <Dashboard {...props} />} /> // authState gone
<Route component={NoMatch} />
</Switch>
);
}
export default Home;
after executing this it renders the Dashboard, and tracing down the component chain with react devtools I can see that the prop has been passed from the Home component to the Switch component successfully. However, once it gets to the Route the prop is gone, and the only props available in the Dashboard component are the history, location and match props.
Why are the props missing and what's the right way to pass them down?
Couple of improvements needed in your code:
Passing props to Switch component is unnecessary
No need to collect router props using the rest syntax only to spread them later
Main Problem:
props inside the function passed to render prop refers to the router props instead of the props passed to Home component. You are using the same identifier for two different things.
Solution
Use different names for router props and the props passed to Home component that you want to pass down to Dashboard component
function Home(props) {
return (
<Switch>
<Route
exact
path="/"
render={(routerProps) => <Dashboard {...routerProps} {...props} />}
/>
<Route component={NoMatch} />
</Switch>
);
}
Alternatively, if you don't need access to router props in the Dashboard component, remove the props parameter from the render prop function.
<Route
exact
path="/"
render={() => <Dashboard {...props} />}
/>
Now, you won't have access to router props inside the Dashboard component but the auth state of the user will be passed down to Dashboard component.
In the most recent versions of the react-router-dom you must replace render and component attributes with element. You cannot pass a callback function there in which there were specified the route props anymore.
Instead you can use a hook in your routed component:
const params = useParams();
to obtain the parameters.
See more in the official documentation.

How can I listen to route change in my main App component, using a custom history object?

I have a header that appears in 95% of pages in my site, so I mount it in my main App component. For the other 5% of pages though I need it to be gone.
I figured a good way to do it would be to change a state from true to false based on the current route, and that state would determine whether the header mounts or not.
at first I tried just using window.location.href as a useEffect dependency but that didn't seem to work. I've read about the option to listen to the location of the history, but I keep getting Cannot read property 'history' of undefined. I thing that perhaps it's because I am using a custom history component, or maybe because I try to do so in my main App component? Not sure.
This is my history object:
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
I use it in my Router like this:
<Router history={history}>
CONDITIONAL COMPONENT
<Switch>
...routes
</Switch>
</Router>
This is how I try to listen, where history is my custom history object
const MyHistory = useHistory(history)
useEffect(() => {
return MyHistory.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`)
})
},[MyHistory])
You are trying to use useHistory within a component that renders Router which is a Provider. In order for useHistory to Work it needs to have a Router higher up in the hierarchy.
So either you wrap the App component with Router like
export default () => (
<Router history={history}><App /><Router>
)
or since you define a custom history you can use it directly without using useHistory
useEffect(() => {
return history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`)
})
},[])
This is how I control every route change in my App. I created a component that listen to the pathname property given by useLocation
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function AppScrollTop() {
const { pathname } = useLocation();
useEffect(() => {
console.log(`You changed the page to: ${pathname}`)
}, [pathname]);
return null;
}
Then I put the component inside the <Router>
<BrowserRouter>
<AppScrollTop />
<Switch>
<Route path="/login" component={Login} />
</Switch>
</BrowserRouter>
I hope this helps.

How to make component not re-render when navigating pages?

In my application there is a main home page that gets loaded and there is another page (Page2.js) I can navigate to via the navbar I have implemented. The problem is that when I go to Page2.js and then go back to the main page, it will re-render all of the components on the main page. I am wondering if there is a way to check the last pathname and say if it is equal to a certain value then all of the components should stay the same?
I have tried using the shouldComponentUpdate method in the navbar (since I do not want it to change) and it is pretty faulty in that it would not read prevProps.location as a value but undefined:
shouldComponentUpdate = (prevProps) => {
if (this.props.location !== prevProps.location) {
return false;
}
}
App.js holds the code for my routing in the application:
// imports here
class App extends Component {
render() {
const {location} = this.props;
return (
<Switch >
<DefaultLayoutRoute exact path="/">
<Redirect to={HomePage}/>
</DefaultLayoutRoute>
<Route exact path={SIGNOUT} render={() => {
auth0Client.signOut();
window.location.href = homeRedirect;
return null
}}>
</Route>
<Route exact path={LOGIN_SUCCESS} component={Callback}/>
<DefaultLayoutRoute exact path={HomePage} location={location.pathname} component={HomePage}
pageName="Office Production"/>
<DefaultLayoutRoute
exact
path={AGENTRANKING}
component={AgentRankings}
pageName="Agents"
/>
<DefaultLayoutRoute exact path={SETTINGS} component={Settings}/>
</Switch>
);
}
}
export default withRouter(App);
Expected Result: To be able to navigate to Page2.js and then back to the main page with all of the components rendered the way it was unless I manually refresh
Actual Results: I go back to the main page and all the components start to re-render.

React Router V4 protected private route with Redux-persist and React-snapshot

I'm implementing private route like so using React Router Route Component:
function PrivateRoute({component: Component, authed, emailVerified, ...rest}) {
return (
<Route
{...rest}
render={props =>
authed === true
? <Component {...props} />
: <Redirect to={{pathname: '/', state: {from: props.location}}} />}/>
)
}
Expected Behavior:
authed is persisted through page refresh through using redux-persist
So on page refresh or reload, if authed prop is true then router should render <Component /> without ever going to path "/"
Actual Behavior which is the Problem:
With authed === true (persisted)
reloading the page or refreshing it leads to the following actions taking place(checked redux devtools)
the action:
"##router/LOCATION_CHANGE" runs and takes it to the correct secure route but then "##router/LOCATION_CHANGE" runs again and it redirects to "/" for a moment and finally
"##router/LOCATION_CHANGE" runs again and directs route back to the secure path, even though authed === true through all this in the redux devtools
Then: My guess was that this error has something to with my main App Component rendering before redux-persist has time to re-hydrate the Redux store.
So I tried doing the following:
I tried delaying my main App component render until my store is re-hydrated using redux-persist like so:
render() {
const {authed, authedId, location, rehydrationComplete} = this.props
return (
<div>
{ rehydrationComplete
? <MainContainer>
<Switch key={location.key} location={location}>
<Route exact={true} path='/' component={HomeContainer} />
<Route render={() => <h2> Oops. Page not found. </h2>} />
</Switch>
</MainContainer>
: <div>...Loading </div> }
</div>
)
}
This effectively fixes the issue above of the path changing when "##router/LOCATION_CHANGE" action runs(only Changes the path keys), However this leads to another Issue with React-snapshot Now: all the static generated html files from react-snapshot Now contain only ...Loading. I tried to set snapshotDelay of 8200 in the react-snapshot options but that didnt solve the issue.
Then:
I tried the following to delay React-snapshot call so that it renders html after the store has been rehydrated:
import {render as snapshotRender} from 'react-snapshot'
import {ConnectedRouter} from 'react-router-redux'
async function init() {
const store = await configureStore()
snapshotRender(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root')
)
registerServiceWorker()
}
init()
But now i get the error: that 'render' from react-snapshot was never called. Did you replace the call to ReactDOM.render()?
I know this is a loaded question, but I want to effectively use these 3 libs(React-Router V4, Redux-persist, React-snapshot) together to serve protected routes without the mentioned errors.
I have something similar to you. Here I use React-Router V4 and a persist-like library.
Your router/routes doesn't need to be aware of the persist. They should rely on your redux's store. The persist should rehydrate your store with all the data.
I didn't see where you are using the PrivateRoute component in your example. Where is it?

location missing in props (ReactJS)

I'm kind of new to reactjs and I'm learning step by step. I'm facing a problem and that is when I try to access the location parameter in the props it returns me undefined. I tried to find the solution but most of the people have written I have to add my component in the router but I'm wrapping it in the router but still, I don't have access to location parameter
export const abcdContainer = withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(abcd));
I'm trying to access the location parameter in the and component but the problem is there is no location parameter in it. Can somebody tell me what is it that is going wrong
Please if anybody know what is wrong please tell me I have spent half of my day and I can't figure it out
CODE AND VERSION ADDED WITH URL
Router version => 2.8.1
URL : http://localhost:3000/somePage?someParam=cm9oYW5qYWxpbHRlYWNoZXJAZ21haWwuY29t
abcdContainer.js
const mapStateToProps = (state, ownProps) => {
// some code over here
}
const mapDispatchToProps = (dispatch, ownProps) => {
// some code over here
};
export const abcdContainer = withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(abcd));
abcd.jsx
class abcd extends Component {
constructor(props, context) {
super(props, context);
this.state = {
// setting state over here
};
}
abcdFunction(){
if (this.props.location.query.someParam !== undefined){ // trying to extract someParam value from location
// some code over here
}
}
render() {
// some code over here
}
}
export default CSSModules(abcd, styles, { allowMultiple: true });
Here is the flow. The router redirect to the container and then the container redirect to the real component
Route.js
export const Routes = ({ store }) => (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={aContainer}>
<IndexRoute component={IContainer} />
// some routes
<Route path="/openAbcd" component={() => (<abcdContainer caller="aaaaaaa" />)} />
// some routes
</Route>
// some routes
</Router>
</Provider>
);
Routes.propTypes = {
store: React.PropTypes.object.isRequired,
};
<Route path="/openAbcd" component={() => (<abcdContainer caller="aaaaaaa" />)} />
If you use an inline arrow function to render your component why you don't just pass the props directly to the component? Then you will not need withRouter(). Like this:
<Route path="/openAbcd" component={props => (<abcdContainer caller="aaaaaaa" {...props} />)} />
Also the docs of react-router v2.8.1 says about withRouter():
A HoC (higher-order component) that wraps another component to provide props.router.
It doesn't provide location but router as a prop. I recommend you to update react-router to v4 or at least v3.
EDIT: "But why were the props not being inserted implicitly?":
React knows two types of components. Stateless functional components and class-based components. Functional components are functions that accept a single props object argument with data and return a React element. Take a look at this line of your code again:
<Route path="/openAbcd" component={() => (<abcdContainer caller="aaaaaaa" />)} />
You passed an arrow function () => (<abcdContainer caller="aaaaaaa" />) to your <Route> element which is an inline definition of a functional component that takes props as a parameter and returns a rendered React element, in this case this is your <abcdContainer>. But as you can see you omitted the props parameter in your function by defining it with empty parenthesis: () => (<AComponent />). React does not automatically pass props to child components that are rendered inside a functional component. When wrapping your <abcdContainer> into an inline functional component you are responsible for passing props to it yourself.
But if you pass the class/variable of your class-based/functional component to the component prop of your <Route> element like you did it in your other routes then it will render this component an implicitly pass props to it because it isn't wrapped in a functional component:
// this will directly render <aContainer> and pass the props to it
<Route path="/" component={aContainer}>
What you did is creating a "functional unnamed wrapper component" that doesn't take any props and also doesn't pass any props further down.
And note that you should not use withRouter() extensively. This HOC is only there to inject a router into components that do not get rendered by a matching route itself and are e.g. much deeper down your component tree. In your case you do not need it.

Categories

Resources