I'm still coming to grips with React and react-router, but I'm wondering how to use react-router to simply update application state without re-rendering DOM nodes.
For example, suppose I have:
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute screen="main" />
<Route path="settings" screen="sync" />
<Route path="overview" screen="overview" />
</Route>
</Router>
I'd like to re-use react-router's matching but simply update the App component's current screen.
The reason is that suppose in the above example I have three screens arranged horizontally with only one visible at a time. When the route changes I want to animate in the appropriate screen. If I do that by assigning a component to each route, react-router doesn't render the other screens until the route matches, and there's a noticeable lag for the screen to slide in.
However, if I keep all three screens in the DOM and simply toggle state to trigger CSS transitions there is no lag (since, with appropriate use of will-change, the browser can pre-render the off-screen layer).
I've tried half a dozen ways of achieving this but they're all very hacky or involve duplicating the matching code somewhat. What am I doing wrong?
Also, I'd like to avoid adding something like Redux just to fix this if possible.
So you want something similar to Spectacle?
In your case I would place all the screens as children of the App component, and use react-router's params to know which screen you're on.
<Route path="/(:screen)" component={App}>
It will be avaialble as a prop to the App component:
class App extends React.Component {
componentWillMount () {
this.props.params.screen
}
}
More about route params at react-router/injected-props
If you're having trouble with re-rendering you can use the component lifecycle method shouldComponentUpdate. Returning false in this method will prevent the component from re-rendering and you'll have the new props (which also include the route params).
class App extends React.Component {
shouldComponentUpdate (nextProps) {
nextProps.params.screen
return false
}
}
Related
I'm using React Router with TypeScript. Here's two routes, namely / and /pages/id.
class MyComponent extends React.Component
{
render()
{
return <BrowserRouter>
<div>
<Route exact path='/' children={
() =>
<TopPage />} />
<Route exact path='/pages/:id' children={
(props: RouteComponentProps<{id: string}>) =>{
console.log(props)
return <IndividualPage match={props.match} />}} />
</div>
</BrowserRouter>
}
}
I expect, since my Routes are exact:
when I access /, only TopPage component is rendered
when I access /pages/1, only IndividualPage component is rendered
when I access /no-pages, nothing is rendered
However, I observed:
when I access /, or /no-pages, IndividualPage component is rendered with match=null (causing Cannot read property 'params' of null error)
when I access /pages/1, both TopPage and IndividualPage components are rendered
What am I doing wrong?
There are four different ways to set the render component for a Route, some of which behave slightly differently than others. Setting the children prop to a render function is one of those ways, but it's not the one that you want based on the behavior you expect. Unlike the other methods, the contents of a children function are always rendered even if the current page is not a match. So you will have both components rendering on every page. Per the docs:
Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function children prop. It works exactly like render except that it gets called whether there is a match or not.
Instead you can use the render prop or the component prop. These will only render if the route is a match.
The default behavior of the Router is to render all Routes which match, not just the first. In practice, this is a non-issue with your example because you have two exact routes so there is no possibility that both will match. But as a habit I would recommend putting your Route components inside of a Switch component which will only render the first matching Route.
class MyComponent extends React.Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={TopPage} />
<Route exact path="/pages/:id" component={IndividualPage} />
</Switch>
</BrowserRouter>
);
}
}
No typescript annotations are needed here because the Route component knows that RouteComponentProps will be passed to the component.
I have React application which has a structure similar to the following.
class App extends Component {
render() {
return (
<div className="App">
<NavBar />
<Router>
<Switch>
<Route path="/login" component={LoginPage} />
<Route path="/" exact component={DashboardPage} />
<Route path="/admin" exact component={AdminPage} />
// many other routes
<Route component={NotFound} />
</Switch>
</Router>
</div>
);
}
}
I do not want the login page to display the <NavBar /> element. I tried using the sessionStorage to get the userId and only display the navigation if the value is set. When I do this and go to the login page and the nav bar is not there. But when I log in, it's still not there. If I refresh however, it will appear.
I know one way to solve this is to make some sort of wrapper around the pages that do want the navigation, but I'd rather not have all of that code duplication, etc.
I feel this must be a common want, and I'm missing something dumb. Any help would be appreciated. I'm new to React so I don't follow everything that's going on here. Thanks.
I think your way of conditionally showing the NavBar is the right way. The question is how to trigger a state change so that the render method takes care of hiding and showing the NavBar, when you log in and out. I suggested maintaining a isLoggedIn state in your App component, and rendering the NavBar based on that, instead of directly accessing the SessionStorage. You could then use a custom event to update the state, when SessionStorage changes.
See this question for updating state based on Storage (in short, you fire and handle a custom event for storage changes): How to listen to localstorage in react.js
This might still be more code that you had hoped for, but it's more aligned with how React works, to derive the view (render) from component state.
I have something like this:
<Route path="/route/:param" component={DealContainer} />
Then while the component is mounted I am doing a client side redirect:
componentWillMount() {
if (this.props.match.params.param != 'desired_one') {
this.props.history.push('/route/desired_one');
Despite the fact that the url changes the component is not remounted...
Any ideas?
You should resolve this issue by using the Redirect component inside "react-router-dom" package
<Route exact path="/route" component={DealContainer} />
<Route
exact
path="/route/desired"
render={() => <Redirect to="/route/desiredRedirectLocation" />}
/>
<Route path="/route/:param" component={DealContainer} />
This implementation should:
Match the exact base route correctly, without matching any props. Lets say that you want to show a list of items (e.g. /products)
Match the desired item to be redirected (e.g. products/car) and redirect it to the desired location, let's say products/horse
Match any other case that you don't want to redirect /products/:id and correctly take the prop you are after inside the match object.
Explanation
The problem with history.push, is that React will figure out you are using the same component, and will only update the differences, without actually re-mounting the component, which is already mounted.
My example with redirect on the other hand, will intercept the route you want to redirect without mounting the component first, so component will be mounted after the redirect happened, correctly executing the action that you need.
I'm using MobX #observer and #withRouter (react-router-v4) wrap page component like this
#withRouter
#inject('stores')
#observer
class Page extends Component {
render() {
return (
<div>
<NavBar />
<Header title={this.props.stores.UIStore.title} />
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/about" component={AboutPage} />
<Route component={NotFound} />
</Switch>
</div>
)
}
Problem
When route location change the NavBar and Header component alway re-render with same props (no any state update). react-perf show many wasted render (no props/state update) when route change.
NavBar include Link and some MobX state (NavBar wrap with #observer+#inject only)
Header is just a stateless component.
Page component require #withRouter cause of #observer (MobX) break react-router (https://github.com/mobxjs/mobx-react/issues/210)
How to prevent NavBar and Header re-render from route location change? Allow re-render only when mobx state update.
I know this is quite old, but that’s how I solved the same problem in my project:
Don’t use withRouter and observer together. Observer implementation of shouldComponentUpdate will return true if location or match objects change.
When you just need a history or location objects use routerStore from mobx-react-router (https://github.com/alisd23/mobx-react-router)
When you need match (usually because of params), make a non-observer component that uses withRouter and pass necessary params to lower levels of hierarchy.
In react router is there a way to have child routes but not display the parent component? I'm trying to display breadcrumbs in my application so I switched to having child routes but I don't want to display the parent component in the way of a 'master/detail' type layout. I just want the child component displayed as if it were navigated to by itself but it seems if you don't include {this.props.children} somewhere in the parent component then then child component never gets rendered.
One hack I've tried is seeing if this.props.children is populated and not displaying the rest of the page in render like
if(this.props.children) {
return (this.props.children)
} else {
return /**rest of render method**/
}
but this feels like a bad solution.
I'm having trouble understanding exactly what you're trying to do, but it seems <IndexRoute> could help you, since that lets you distinguish what you do at /portal vs. /portal/feature
<Route path="/portal" component={ComponentContainingBreadcrumbs}>
<IndexRoute component={ComponentWithRestofRenderMethodFromYourExample} />
<Route path="child1" component={Child1} />
<Route path="child2" component={Child2} />
</Route>