For a long time I have been trying to get routing to work in our app after an error boundary has been hit, but only today did I find the code that was seemingly identical to the many examples lying around had one important difference: the routes were wrapped by a Switch. This simple change is enough to stop routing from working, if enabled. Demo
Take the following snippet. If I remove the Switch bit, this works fine even if each component should fail, but not if wrapped by the switch. I would like to know why.
<div style={{ backgroundColor: "#ffc993", height: "150px" }}>
<Switch>
<Route
path="/"
exact
render={() => (
<ErrorBoundary>
<MyComponent1 title="Component 1" />
</ErrorBoundary>
)}
/>
<Route
path="/comp1"
render={() => (
<ErrorBoundary>
<MyComponent1 title="Component 1 Again" />
</ErrorBoundary>
)}
/>
<Route
path="/comp2"
render={() => (
<ErrorBoundary>
<MyComponent2 title="Component 2" />
</ErrorBoundary>
)}
/>
</Switch>
Basically, this problem boils down to how React does reconciliation.
When a component updates, the instance stays the same, so that state is maintained across renders. React updates the props of the underlying component instance to match the new element
Say we have this example app:
<App>
<Switch>
<Route path="a" component={Foo}/>
<Route path="b" component={Foo}/>
</Switch>
</App>
This will, somewhat unintuitively, reuse the same instance of Foo for both routes! A <Switch> will always return the first matched element, so basically when React renders this is equivalent of the tree <App><Foo/></App> for path "a" and <App><Foo/></App> for path "b". If Foo is a component with state, that means that state is kept, as the instance is just passed new props (for which there are none, except children, in our case), and is expected to handle this by recomputing its own state.
As our error boundary is being reused, while it has state that has no way of changing, it will never re-render the new children of its parent route.
React has one trick hidden up its sleeve for this, which I have only seen explicitly documented on its blog:
In order to reset the value when moving to a different item (as in our password manager scenario), we can use the special React attribute called key. When a key changes, React will create a new component instance rather than update the current one.
(...) In most cases, this is the best way to handle state that needs to be reset.
I was first hinted to this by a somewhat related issue on Brian Vaughn's error bondary package:
The way I would recommend resetting this error boundary (if you really want to blow away the error) would be to just clear it out using a new key value. (...) This will tell React to throw out the previous instance (with its error state) and replace it with a new instance.
The alternative to using keys would be to implement either exposing some hook that could be called externally or by trying to inspect the children property for change, which is hard. Something like this could work (demo):
componentDidUpdate(prevProps, prevState, snapshot) {
const childNow = React.Children.only(this.props.children);
const childPrev = React.Children.only(prevProps.children);
if (childNow !== childPrev) {
this.setState({ errorInfo: null });
}
But it's more work and much more error prone, so why bother: just stick to adding a key prop :-)
To give you the shortcut of this fix, please see the new "key" prop on each of the ErrorBoundary component and each must be unique, so the code should look like this:
<Switch>
<Route
path="/"
exact
render={() => (
<ErrorBoundary key="1">
<MyComponent1 title="Component 1" />
</ErrorBoundary>
)}
/>
<Route
path="/comp1"
render={() => (
<ErrorBoundary key="2">
<MyComponent1 title="Component 1 Again" />
</ErrorBoundary>
)}
/>
<Route
path="/comp2"
render={() => (
<ErrorBoundary key="3">
<MyComponent2 title="Component 2" />
</ErrorBoundary>
)}
/>
</Switch>
To elaborate, the answer of #oligofren is correct. Those 3 ErrorBoundary components are the same instances but may differ in props. You can verify this by passing "id" prop to each of the ErrorBoundary components.
Now you mentioned why is that if you remove Switch component, it works as expected? Because of this code:
https://github.com/ReactTraining/react-router/blob/e81dfa2d01937969ee3f9b1f33c9ddd319f9e091/packages/react-router/modules/Switch.js#L40
I recommend you to read the official documentation of React.cloneElement here: https://reactjs.org/docs/react-api.html#cloneelement
I hope this gives you an idea on this issue. Credit to #oligofren as he explained in more details about the idea of the instances of those components.
Related
I have a React Route with significant user input that I want to use the Prompt component with. I'm importing it from react-router-dom without error and adding the component to the render method of the class component, but no matter what, the prompt alert never actually appears and the user can freely navigate off the page without seeing any alert.
I've tried:
Putting the component in with when={aStateBooleanVariable} as well as when={true} and no when attribute at all.
Using a function for the message.
Ensuring that the Route it's on is an exact path.
Importing it from react-router instead of react-router-dom.
Moving the component around within the render method.
Moving the Prompt into the actual Switch statement itself in the App component that wraps everything in BrowserRouter.
Nothing I've tried has gotten the prompt to actually show up or prevent the user from navigating off the page and I've been unable to find anything in the docs or extensive searching where anyone else has had this problem. Does anyone have any idea why this just isn't even starting to work?
I'm not really sure what code to put since it's all pretty much boilerplate react-router-dom.
This is my App.jsx switch statement:
return (
<BrowserRouter>
<header>
{loggedInUser ? <NavbarComp loggedInUser={loggedInUser} setLoggedInUser={setLoggedInUser} /> : null}
</header>
<Switch>
<PrivateRoute path="/ged/campaigns/new" component={CampaignNew} loggedInUser={loggedInUser} />} />
<PrivateRoute path="/ged/campaigns/:id" component={Campaign} loggedInUser={loggedInUser} />} />
<PrivateRoute path="/ged/characters/new" component={CharGen} loggedInUser={loggedInUser} />} />
<PrivateRoute path="/ged/characters/:id" component={CharacterMain} loggedInUser={loggedInUser} />} />
<PrivateRoute path="/ged" component={GEDHome} loggedInUser={loggedInUser} />} />
<Route path="/" component={Home} loggedInUser={loggedInUser} setLoggedInUser={setLoggedInUser} />} />
<Route component={DeadPage} />
</Switch>
</BrowserRouter>
)
I'm importing the component as:
import {Prompt} from 'react-router-dom';
And incorporating the component at the top of its parent's render method return:
<Prompt message="You have changes that will be lost if you leave without saving." />
What other code might be making it fail silently like this?
I have the following routes defined:
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/project/:id/search" component={SearchPage} />
<Route path="/project/:id/upload" component={UploadPage} />
</Switch>
The issue is that search results (local state of the SearchPage component) remain when you navigate to another project's search page. I could solve it by manually clearing state in willReceiveProps when the ID changes but I want to avoid that as there are several sub-pages of /project that would need the logic.
Using render={()=> <SearchPage />} made no difference.
What's a clean way of ensuring that my components get re-initialized when the parameter in the URL changes?
You may use 'key reset state' technique: if special prop key is different React will for sure recreate element instead of updating.
So it may be
<Route
path="/project/:id/search"
render={({ match: {params: {id} } })=> <SearchPage key={id} />}
/>
To me this affects readability in bad way. So using componentDidUpdate(willReceiveProps is deprecated) seems to be better maintainable.
I'm trying to create an app with Reactjs + Redux + React router but I'm having some problems that I don't understand what it's causing it. Most probably I'm not fully understand how it works.
When I update the store of Redux all the components get re rendered instead of the ones where the state it's used. That means that my api calls for example are running twice when I simple show a flash message. EG:
render() {
const user = this.props.user;
if( ! user || ! user.token ) {
return (<Redirect
to={{
pathname: "/login",
state: {from: this.props.location}
}}
/>)
}
return (
<div className="app">
<Header {...this.props} />
<FlashMessage message={this.props.flash.message} type={this.props.flash.msg_type} {...this.props}/>
<div className="app-body">
<Sidebar {...this.props}/>
<main className="main">
<Breadcrumb />
<Container fluid>
<Switch>
<Route path="/settings/permissions/add" name="Add Permission"
component={() => <AddPermissionView {...this.props}/>}/>
<Route path="/settings/permissions/" name="Permissions"
component={() => <ListPermissions {...this.props}/>}/>
<Route path="/dashboard" name="Dashboard"
component={() => <Dashboard {...this.props}/>}/>
<Route path="/logout" name="Logout" component={() => <Redirect to="/login"/>}/>
<Redirect from="/" to="/dashboard"/>
</Switch>
</Container>
</main>
<Aside />
</div>
<Footer />
</div>
);
}
So for example if I update the store for flash message, the Flashmessage compoenent gets rendered as it should, but also the Dashboard,sidebar,Header, etc.
I thought only the state that changed it's rendered again.
That means I need to use shouldComponentUpdate on every component I create to avoid that behaviour ?
Render Prop and Component Prop inside of Route.
When you use component instead of render the router uses React.createElement to create a new React element from the given component.
if you provide an inline function to the component prop, you would create a new component every render.
This results in the existing component unmounting and the new component mounting instead of just updating the existing component.
Solution
Replace component props with the render prop in your routes.
<Route path="/settings/permissions/add" name="Add Permission" render={() => <AddPermissionView {...this.props}/>}/>
<Route path="/settings/permissions/" name="Permissions" render={() => <ListPermissions {...this.props}/>}/>
<Route path="/dashboard" name="Dashboard" render={() => <Dashboard {...this.props}/>}/>
<Route path="/logout" name="Logout" render={() => <Redirect to="/login"/>}/>
By default, updates to the Redux Store cause application-wide re-renders, regardless of where the update was applied in the Store. In case of small applications, this behavior is typically not noticeable. However, as an application (and its state tree) grows, its performance can be easily hampered by that blanket "re-render every component" strategy.
Fortunately, there are solutions to this problem. I use React Reselect, which helps create memoized selectors that help localize renders to components whose state was affected, leaving everything else unchanged:
https://github.com/reactjs/reselect
I'd suggest reading through the text on the page, perhaps twice, watching the video that's linked in the documentation, and actually doing an example project to understand how Redux works naturally, and how Reselect gets rid of unnecessary component re-renders via memoized selectors.
Id like to use my app entry point as a global state store. Passing info down to children as props.
Using react-router 4, how can I send prop data down to the rendered components. In a similar fashion to this:
<Route Path=“/someplace” component={someComponent} extra-prop-data={dataPassedToSomeComponent} />
I’ve seen some janky workarounds for older versions of react-router, that appear to be deprecated.
What is the correct way of doing this in v4?
You can pass in a function to the render prop instead of passing in the component directly to the component prop.
<Route path="/someplace" render={() => <SomeComponent props={} />} />
You can read more here.
And to add to the above answer, if you want the routing properties accessible to the component you need to include those. Now when the router activates "SomeComponent", the component will get all the routing props plus the extra param(s) - in this example "param".
<Route path='/someplace' component={(props) => <SomeComponent param="yo" {...props}/>} />
Technically there is 2 ways to do it.
The first one (not the most efficient) is to pass an inline function to the component prop:
<Route
path=“/someplace”
component={(props) => (
<SomeComponent {...props} extra-prop-data={ dataPassedToSomeComponent } />
) />
The second one is the best solution. To prevent create a new component on every render like on the first exemple, we pass the same inline function but this time to the render prop:
<Route
path=“/someplace”
render={(props) => (
<SomeComponent {...props} extra-prop-data={ dataPassedToSomeComponent } />
) />
I have the following JSON object...
{ name: "Jessie" }
And I want to be able to pass it through my Router so that it can be displayed on my pages. For example, this is my root page...
StaticPage.jsx
export default class StaticPage extends React.Component {
render() {
return (
<div>
<Router history={hashHistory}>
<Route path='/' component={Search} />
<Route path='/favorites' component={Favorites} />
</Router>
</div>
);
}
}
So passing this data to Search, I would imagine might look something like this...
<Route path='/' component={Search} name = {this.props.name}/>
However, nothing gets rendered when I do that. I have researched this quite a bit and understand, from what I've read, that you cannot pass objects through the Router. It's very odd bc Router looks like a traditional React component but does not function as such. None of the explanations of a work around seem clear to me. Could anyone provide me with an example using this code? I am using react-router 3.0. There didn't seem to be any magical solution with 4.0 so I figured I'd ask before upgrading. Thanks!
It's because the component prop of <Route> only renders the component with route props, not your supplied props.
You can use the render or component prop on a <Route> in React Router v4 to pass a function which returns a <Search> element that explicitly passes the name:
<Route path="/" render={() => <Search name={this.props.name} />} />
Or with component:
<Route path="/" component={() => <Search name={this.props.name} />} />
But you should prefer render or else you'll have lots of remounting. If you still plan to use route props, you can also do:
render={routeProps => <Search name={this.props.name} {...routeProps} />}
A whole different approach, one more elegant in my opinion is to use route params and pass the name directly through the URL:
<Route path="/:name" component={Search} />
When you navigate to /Bob, you can access this.props.match.params.name which'll give you "Bob".
It is not a good practice to pass the object data via the routes directly. It is recommended to pass a param such as name or id in this way:
<Route path='/favorites/:name' component={Favorites} />
And retrieve it from your request in the destination.
This is a duplicate issue: Pass object through Link in react router