React component being re rendered - reactjs

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.

Related

Stop re-rendering components when route changes. Especially components outside Routes

I have the following components structure:
App
Header
Home
Categories
Products
ProductList
ProductDetail
ProductDetailMore
Main Route:
<div>
<Header />
<Switch>
<Route exact path="/" render={() => <Home />} />
<Route path="/category" render={() => <Categories />} />
<Route path="/products" render={() => <Products />} />
</Switch>
</div>
The Route in Products:
const productsData = [...];
return (
<div>
<div>
<ProductList data={productsData} />
</div>
<Switch>
<Route path={`${match.url}/:productId`} render={()=>
<ProductDetail data={productsData} />} />
<Route exact path={match.url} render={()=> (
<div style={{ textAlign: "center" }}>Select a product.</div>
)} />
</Switch>
</div>
);
And the route in ProductDetail:
return (
<>
<div>
<h3> {product.name} </h3>
Links:
<div>
<NavLink activeClassName="active" to={`${match.url}/detail/${ "100"}`}>
Click me
</NavLink>
</div>
</div>
<div>
<Route path={`${match.url}/detail/:code`} render={()=>
<ProductDetailMore />} />
</div>
</> );
When I click on 'Click me' the value 100 is correctly displayed in my ProductDetailMore component but all the components are re-rendered (Products, ProductList, ProductDetail, ProductDetailMore); so, my questions is, how can I prevent a re-rendering in the parent components [Products, ProductDetail]?
And especially, I would like to avoid a re-render in ProductList, the one that is not in a Route?
You cannot really generally avoid rerenders, as react already decides which components need a rerender. However, this only means you can't avoid rerendering the parent components. With the react dev tools you can analyze why they rerender (it's an option in the profiler) and possibly find unnecessary causes for rerenders.
But these are the good news:
What you can easily do is preventing sub-components to rerender. For Example "ProductList". One Way would be the React.memo HOC used directly in the export of the component or in the "Categroies" component (at a static location, not in the render function):
const ProductListMemo = React.memo(ProductList)
Then, you're using "ProductListMemo" in the render function. This way, when the component is rendered, React checks beforehand if any props changed. If not, the component is not rerendered. Your code is somehow incomplete though. You define
const productsData = [...]
If this is in the render function, a new array will always be created and even if the contents are the same, React.memo will see a new array and rerender the component. You have to move the array outside of the render function or you have to wrap it in a useMemo (if you're not using class components):
const productsData = useMemo(() => [...], []);
This "useMemo" hook can also be used to avoid rerenders of components, you could use
{useMemo(() => (<div>
<ProductList data={productsData} />
</div>), [productsData])}
This way, every rerender react checks if "productsData" changed, and only then rerenders the components.
So the important thing to know is that if a parent component rerenders because of a state update for example, it will rerender every child component. And these will also rerender every of their child components. With a React.memo or a useMemo however, you can help react to decide to use a previously rendered component instead.

Error Boundaries disables routing inside of a Switch

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.

How do I pass state to components when using a `react-router` config

I'm using a route config as defined here. But, when using this method of router configuration how do I pass props to the components?
Looking at the following method from the docs there doesn't seem to be a way to provide props:
const RouteWithSubRoutes = (route) => (
<Route path={route.path} render={props => (
// pass the sub-routes down to keep nesting
<route.component {...props} routes={route.routes}/>
)}/>
)
Doing something like the following doesn't work. The prop doesn't get passed through:
{routes.map((route, i) => (
<RouteWithSubRoutes key={i} user={this.state.user} {...route}/>
))}
I am using React's default state management. Do I need Redux for this?
Here is example of how to use Flux + React together to pass props. I hope this is useful for you. Let me know if you have other questions.
AppComponent.js
In your AppComponent, you would need to pass only the pages you need to render.
render () {
<div>
<Route path="/login" component={Login} />
<Route exact path= "/:date?" component={Dashboard} />
</div>
}
In your app.js component, you import all services, actions, and main store component you need. Checkout bottle.js for easier way to pass values and services. However, you would need to just render
ReactDOM.render(
(
<BrowserRouter>
<Route path="/" AppStore={AppStore} render={props => <AppComponent {...props} AppStore={AppStore} />} />
</BrowserRouter>
),
document.getElementById("view-container")
);
You should let the parent component like the component Dashboard in AppComponment.js get the data passed from app.js to AppComponent to Dashboard (and Dashboard's children components).
As for AppStore, this would be like a container for all your other stores. You would need import all your other data store components and actions.

How to Pass Data Through My React-Router with ReactJS?

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

React-Router v4 shared layout re-renders

I know that this question has been asked before, but I keep having issues with this.
The issue I have is that when I use a Page Layout-like component to wrap my routes, this page layout is re-rendered when changing path.
In react-router v3 I did something like this:
<Router history={this.props.history}>
<Route path="/">
<IndexRedirect to="/Dossiers" />
<Route path="/Dossiers" component={MainLayout}>
<IndexRoute component={DossiersPage} />
<Route path="/Dossiers/:dossierId/:title" component={DossierDetailsPage} />
</Route>
</Route>
</Router>
When moving paths, this would NOT re-render the MainLayout component (which is easily checked by putting something in state inside MainLayout).
Now, in react-router v4 I tried a couple of approaches already:
Wrapping Switch with the MainLayout component
Creating a RouteWithMainLayout component which wraps Route (as described here: https://simonsmith.io/reusing-layouts-in-react-router-4/)
Some of the approaches described here: https://github.com/ReactTraining/react-router/issues/3928
However, all solutions I've tried seem to re-render the MainLayout component, basically causing state to reset to its initial value(s).
tldr; How do I create a wrapping component in react-router v4 which doesn't re-render when changing paths
I put together a codesandbox example of how I'm using a "page layout" type of component. It uses React Router v4.1.2.
https://codesandbox.io/s/Vmpy1RzE1
As you described in your question, and as was described in Matt's answer, the MainLayout component wraps the routes.
<BrowserRouter>
<MainLayout>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
</Switch>
</MainLayout>
</BrowserRouter>
It is true that the MainLayout component re-renders when I navigate the app, in the sense that render is called by React. But, the MainLayout component is never unmounted, so the state never re-initializes.
I've placed some console.logs around my example to show this. My MainLayout looks like this:
export default class MainLayout extends React.Component {
state = {
layoutCreatedOn: Date(),
};
componentDidMount() {
//This will not fire when we navigate the app.
console.log('layout did mount.');
}
componentWillUnmount() {
//This won't fire,
// because our "shared page layout" doesn't unmount.
console.log('layout will unmount');
}
render() {
//This does fire every time we navigate the app.
// But it does not re-initialize the state.
console.log('layout was rendered');
return (
<div styles={styles}>
<h5>
Layout created: {this.state.layoutCreatedOn}
</h5>
<Sidebar />
{this.props.children}
</div>
);
}
}
As you click around the app, you'll see a few things.
componentDidMount fires only once.
componentWillUnmount never fires.
render fires every time you navigate.
Despite this, my layoutCreatedOn property shows the same time as I navigate the app. The state is initialized when the page loads, and never re-initialized.
You no longer need IndexRedirect, instead just wrap all of your routes in your MainLayout component, such as:
<Router history={this.props.history}>
<Switch>
<MainLayout>
<Route path="/" component={DossiersPage}/>
<Route path="/Dossiers/:dossierId/:title" component={DossierDetailsPage} />
</MainLayout>
</Switch>
</Router>
Here is the correct solution for React Router v4 as stated here
So basically you need to use the render method to render the layout and wrap your component like this:
<Router>
<Switch>
<Route path={ROUTES.LOGIN} render={props =>
<LoginLayout {...props}>
<Login {...props} />
</LoginLayout>
} />
<Route path={ROUTES.REGISTER} render={props =>
<LoginLayout {...props}>
<Register {...props} />
</LoginLayout>
} />
<Route path="*" component={NotFound} />
</Switch>
</Router>
This will not cause re-rendering of the layout when you are changing the routes.
When you have many different components with many different layouts you can go ahead and define them in a route config array like the example from the issue I linked:
const routes = [
{ path: '/',
exact: true,
component: Home
},
{ path: '/about',
component: About,
},
{ path: '/cart',
component: Three,
}
]
<Router>
<Switch>
{routes.map({ path, exact, component: Comp } => (
<Route path={path} exact={exact} render={(props) => (
<LayoutWithSidebarAndHeader {...props}>
<Comp {...props}/>
</LayoutWithSidebarAndHeader>
)}/>
))}
<Route component={Error404}/>
</Switch>
</Router>

Resources