Prevent react route unmounting component on state change - reactjs

I'm using react-router (v.4.3.1) to render the main part of my application and I have a drawer on the left side with the menu. When a button is toggled in the app header I'm changing the state of the collapsed variable so that the components re-render the css accordantly. My problem is this variable needs to be stored on the component rendering all my Route and when the component is updated Route is unmounting and mounting it's component.
I've already tried to provide a key to my Route but it's not working.
My code looks like this and the parent of this component is the one being updated which re-renders my Main component:
class Main extends Component {
constructor(props) {
super(props);
this.observer = ReactObserver();
}
getLayoutStyle = () => {
const { isMobile, collapsed } = this.props;
if (!isMobile) {
return {
paddingLeft: collapsed ? '80px' : '256px',
};
}
return null;
};
render() {
const RouteWithProps = (({index, path, exact, strict, component: Component, location, ...rest}) =>
<Route path={path}
exact={exact}
strict={strict}
location={location}
render={(props) => <Component key={"route-" + index} observer={this.observer} {...props} {...rest} />}/>
);
return (
<Fragment>
<TopHeader observer={this.observer} {...this.props}/>
<Content className='content' style={{...this.getLayoutStyle()}}>
<main style={{margin: '-16px -16px 0px'}}>
<Switch>
{Object.values(ROUTES).map((route, index) => (
<RouteWithProps {...route} index={index}/>
))}
</Switch>
</main>
</Content>
</Fragment>
);
}
}
I would like the Route just to update and not to unmount the component. is this possible?

you are having this issue due to defining RouteWithProps inside of render method. This causes React to unmount old and mount a new one each time render method is called. Actually creating component dynamically in the render method is a performance bottleneck and is considered a bad practice.
Just move the definition of RouteWithProps out of Main component.
Approximate code structure will look like:
// your impors
const RouteWithProps = ({observer, path, exact, strict, component: Component, location, ...rest}) =>
<Route path={path}
exact={exact}
strict={strict}
location={location}
render={(props) => <Component observer={observer} {...props} {...rest} />}/>;
class Main extends Component {
...
render(){
...
{Object.values(ROUTES).map((route, index) => (
<RouteWithProps key={"route-" + index} {...route} observer={this.observer}/>
))}
^^^ keys should be on this level
...
}
}

Related

React router is not mounting the component

I'm using React Router for routing to different routes as below:
<Router>
<Switch>
<Route path="/teams/:teamName/matches" >
<MatchPage/>
</Route>
<Route path="/teams/:teamName" >
<TeamPage/>
</Route>
</Switch>
</Router>
Now in my TeamPage component I'm calling an API using async and then in the render method invoking another component called MatchDetailCard
class TeamPage extends Component {
constructor(props) {
console.log('const called')
super(props)
this.state = {
team: [],
teamName:null
}
}
async componentDidMount() {
console.log(this.props.match.params.teamName);
const teamName = this.props.match.params.teamName;
const response = await fetch(`http://localhost:8080/team/${teamName}`);
const json = await response.json();
console.log(json);
this.setState({team:json, teamName: teamName});
}
componentDidUpdate () {
console.log('updated')
}
render() {
if (!this.state.team || !this.state.team.teamName) {
return <h1>Team not found</h1>;
}
return (
<div className="TeamPage">
<div className="match-detail-section">
<h3>Latest Matches</h3>
<MatchDetailCard teamName={this.state.team.teamName} match={this.state.team.matches[0]}/>
</div>
</div>
);
}
}
export default withRouter(TeamPage);
Within the MatchDetailCard component I create a router Link to the same TeamPage component which will have a different teamName this time as below:
const MatchDetailCard = (props) => {
if (!props.match) return null;
const otherTeam = props.match.team1 === props.teamName ? props.match.team2 : props.match.team1;
const otherTeamRoute = `/teams/${otherTeam}`;
const isMatchWon = props.teamName === props.match.matchWinner;
return (
<div className={isMatchWon ? 'MatchDetailCard won-card' : 'MatchDetailCard lost-card'}>
<div className="">
<span className="vs">vs</span><h1><Link to={otherTeamRoute}>{otherTeam}</Link></h1>
</div>
</div>
);
}
export {MatchDetailCard};
The problem that I'm facing is that the on-click of the link to /team/teamName route only the TeamPage component is not mounting instead it's just getting an update.
I want to have the call to componentDidMount hook to make the API call again in this scenario.
What's the problem with my logic?
If the same component is used as the child of multiple <Route>s at the same point in the component tree, React will see this as the same component instance and the component’s state will be preserved between route changes. If this isn’t desired, a unique key prop added to each route component will cause React to recreate the component instance when the route changes.
https://reactrouter.com/web/api/Route
You can add the teamName as a key prop on the component, which will tell React to unmount/mount the component when the key value changes.
<Router>
<Switch>
<Route
path="/teams/:teamName/matches"
render={({ match }) => {
return <MatchPage key={match.params.teamName} />;
}}
/>
<Route
path="/teams/:teamName"
render={({ match }) => {
return <TeamPage key={match.params.teamName} />;
}}
/>
</Switch>
</Router>

How to store variables that can be accessed and modified across pages and components with Ionic React?

I am writing an app using the Ionic Framework and React. It's a multi-page app that I'm using IonReactRouter and Routes to handle navigation for. The app needs to keep track of a language preference (english or spanish) and instead of passing down it through props through my many pages and components, I'm trying to figure out how to store it as a global variable. I will also be working on allowing the user to add tables, and would like to store their data in a database (using the SQlite plug in potentially) so I'm thinking I need some sort of context or store to handle both.
Here's what I've tried:
Using React Context, both with hooks in functional components and in class components. I've tried consuming the state with
static contextType = AppContext;
and then referring to this.context to consume the state, and I've used hooks as well, but neither have worked properly.
I'll include what I have right now.
I believe in the page that manipulates th
Here's my App.js
export default class App extends Component{
constructor(props){
super(props);
this.site = "baja";
this.setLanguage = (lang) => {
this.setState(state => ({
language: lang
}));
};
this.state = {
language: "english",
setLanguage: this.setLanguage,
};
}
render(){
return(
<AppContextProvider value>
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/" render={() => <Redirect to="/home/sections" />} />
<Route path="/speciesList/community/:name/:community"
strict={true}
render={(props) => <SpeciesList community={true} site={this.site} {...props}/>}/>
// there's a lot of routes here but I only included the relevant ones
<Route path="/home/:tab"
strict={true}
render={(props) => <Home site={this.site} {...props}/>}/>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
</AppContextProvider>
);
}
}
And my AppContext.js
import React from 'react';
export const AppContext = React.createContext({
language: "english",
setLanguage: () => {}
});
And the component I'm modifying context with
class TranslationButton extends Component {
render(){
return (
<AppContext.Consumer>
{({language, setLanguage}) => (
<IonRadioGroup color="dark" value={language} onIonChange={(e) => setLanguage(e.detail.value)}>
<IonItem><IonLabel>Set Language</IonLabel></IonItem>
<IonItem><IonLabel>English</IonLabel><IonRadio slot="start" value="english"/></IonItem>
<IonItem><IonLabel>Español</IonLabel><IonRadio slot="start" value="espanol"/></IonItem>
</IonRadioGroup>
)}
</AppContext.Consumer>
)
}
}
export default TranslationButton;
The component that reflects changes to the context language:
export default class SectionsTab extends Component {
render() {
return (
<AppContext.Consumer>
{({language, setLanguage}) => (
<>
<Toolbar site={this.props.site} title="Home" history={this.props.history} language={this.context}/>
<IonContent fullscreen>
{language}
<PageList title="Sections"
site={this.props.site}
items={(this.props.site === "pingree") ? pingree_sections : baja_sections}/>
</IonContent>
</>
)}
</AppContext.Consumer>
)
}
}
The component that does not show the correct context language after changing it:
class SpeciesList extends Component {
constructor(props){
super(props);
this.state = {
nameOrder: "scientific",
alphabetOrder: "a-z",
sortOpen: false
}
this.category = this.props.match.params.name;
if(this.props.community)
this.community = this.props.match.params.community;
if(this.props.family)
this.family = this.props.match.params.family;
this.section = new Section(this.category, this.family, this.community, props.site);
}
render() {
return (
<AppContext.Consumer>
{({language, setLanguage}) => (
<IonPage>
<Toolbar site={this.props.site} history={this.props.history} title={this.section.title}/>
<IonContent fullscreen>
{language}
{this.renderSortButton()}
<IonList>
{this.section.data.map((item, index) =>
<IonItem href={getURL(index, this.category)} key={index}>
<IonThumbnail item-left><img alt={item.species} src={findThumbnail(item, this.props.site, this.category)}/></IonThumbnail>
{this.renderLabel(item)}
</IonItem>)
}
</IonList>
</IonContent>
</IonPage>)}
</AppContext.Consumer>
);
};
And then when I try to just access and display the context, I use a similar thing as above, using the AppContextConsumer tag and then referring to language. The thing is, in the page that is the parent (or grandparent, somewhere upwards in terms of rendering) of the TranslationButton.js component above, the context is correctly consumed, and when I change the language in the context, that's reflected. But, in a separate page (SpeciesList) that consumes the context in the same way, it doesn't reflect the change based on the user action with the button in TranslationButton. I'm not sure if navigating to a different page rerenders everything and resets the context or what, but I'm not sure what's happening.
In terms of the components - the SpeciesList is a page (loaded with the route) and the TranslationButton is rendered by a Toolbar which is rendered by the SectionsTab class. So I don't know if the nested components matter either.

React prevent remounting components passed from props

When using React with React Router I run in some mounting issues.
This might not even be a problem with React Router itself.
I want to pass some additional data along with the child routes.
This seems to be working, however the changes on the main page trigger grandchildren to be remounted every time the state is changed.
Why is this and why doe this only happen to grandchildren an not just the children ?
Code example:
import React, { useEffect, useState } from 'react';
import { Route, Switch, BrowserRouter as Router, Redirect } from 'react-router-dom';
const MainPage = ({ ChildRoutes }) => {
const [foo, setFoo] = useState(0);
const [data, setData] = useState(0);
const incrementFoo = () => setFoo(prev => prev + 1);
useEffect(() =>{
console.log("mount main")
},[]);
useEffect(() =>{
setData(foo * 2)
},[foo]);
return (
<div>
<h1>Main Page</h1>
<p>data: {data}</p>
<button onClick={incrementFoo}>Increment foo {foo}</button>
<ChildRoutes foo={foo} />
</div>
);
};
const SecondPage = ({ ChildRoutes, foo }) => {
const [bar, setBar] = useState(0);
const incrementBar = () => setBar(prev => prev + 1);
useEffect(() =>{
console.log("mount second")
},[]);
return (
<div>
<h2>Second Page</h2>
<button onClick={incrementBar}>Increment bar</button>
<ChildRoutes foo={foo} bar={bar} />
</div>
);
};
const ThirdPage = ({ foo, bar }) => {
useEffect(() =>{
console.log("mount third")
},[]);
return (
<div>
<h3>Third Page</h3>
<p>foo: {foo}</p>
<p>bar: {bar}</p>
</div>
);
};
const routingConfig = [{
path: '/main',
component: MainPage,
routes: [
{
path: '/main/second',
component: SecondPage,
routes: [
{
path: '/main/second/third',
component: ThirdPage
},
]
}
]
}];
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
</Route>
);
})}
</Switch>
);
};
export const App = () => {
return(
<Router>
<Routing routes={routingConfig}/>
<Route exact path="/">
<Redirect to="/main/second/third" />
</Route>
</Router>
)
};
export default App;
Every individual state change in the MainPage causes ThirdPage to be remounted.
I couldn't create a snippet with StackOverflow because of the React Router. So here is a codesandbox with the exact same code: https://codesandbox.io/s/summer-mountain-unpvr?file=/src/App.js
Expected behavior is for every page to only trigger the mounting once.
I know I can probably fix this by using Redux or React.Context, but for now I would like to know what causes this behavior and if it can be avoided.
==========================
Update:
With React.Context it is working, but I am wondering if this can be done without it?
Working piece:
const ChildRouteContext = React.createContext();
const ChildRoutesWrapper = props => {
return (
<ChildRouteContext.Consumer>
{ routes => <Routing routes={routes} {...props} /> }
</ChildRouteContext.Consumer>
);
}
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<ChildRouteContext.Provider value={routes}>
<Component {...rest} ChildRoutes={ChildRoutesWrapper}/>
</ChildRouteContext.Provider>
</Route>
);
})}
</Switch>
);
};
To understand this issue, I think you might need to know the difference between a React component and a React element and how React reconciliation works.
React component is either a class-based or functional component. You could think of it as a function that will accept some props and
eventually return a React element. And you should create a React component only once.
React element on the other hand is an object describing a component instance or DOM node and its desired properties. JSX provide
the syntax for creating a React element by its React component:
<Component someProps={...} />
At a single point of time, your React app is a tree of React elements. This tree is eventually converted to the actual DOM nodes which is displayed to our screen.
Everytime a state changes, React will build another whole new tree. After that, React need to figure a way to efficiently update DOM nodes based on the difference between the new tree and the last tree. This proccess is called Reconciliation. The diffing algorithm for this process is when comparing two root elements, if those two are:
Elements Of Different Types: React will tear down the old tree and build the new tree from scratch // this means re-mount that element (unmount and mount again).
DOM Elements Of The Same Type: React keeps the same underlying DOM node, and only updates the changed attributes.
Component Elements Of The Same Type: React updates the props of the underlying component instance to match the new element // this means keep the instance (React element) and update the props
That's a brief of the theory, let's get into pratice.
I'll make an analogy: React component is a factory and React element is a product of a particular factory. Factory should be created once.
This line of code, ChildRoutes is a factory and you are creating a new factory everytime the parent of the Component re-renders (due to how Javascript function created):
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
Based on the routingConfig, the MainPage created a factory to create the SecondPage. The SecondPage created a factory to create the ThirdPage. In the MainPage, when there's a state update (ex: foo got incremented):
The MainPage re-renders. It use its SecondPage factory to create a SecondPage product. Since its factory didn't change, the created SecondPage product is later diffed based on "Component Elements Of The Same Type" rule.
The SecondPage re-renders (due to foo props changes). Its ThirdPage factory is created again. So the newly created ThirdPage product is different than the previous ThirdPage product and is later diffed based on "Elements Of Different Types". That is what causing the ThirdPage element to be re-mounted.
To fix this issue, I'm using render props as a way to use the "created-once" factory so that its created products is later diffed by "Component Elements Of The Same Type" rule.
<Component
{...rest}
renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>
Here's the working demo: https://codesandbox.io/s/sad-microservice-k5ny0
Reference:
React Components, Elements, and Instances
Reconciliation
Render Props
The culprit is this line:
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
More specifically, the ChildRoutes prop. On each render, you are feeding it a brand new functional component, because given:
let a = props => <Routing routes={routes} {...props}/>
let b = props => <Routing routes={routes} {...props}/>
a === b would always end up false, as it's 2 distinct function objects. Since you are giving it a new function object (a new functional component) on every render, it has no choice but to remount the component subtree from this Node, because it's a new component every time.
The solution is to create this functional component once, in advance, outside your render method, like so:
const ChildRoutesWrapper = props => <Routing routes={routes} {...props} />
... and then pass this single functional component:
<Component {...rest} ChildRoutes={ChildRoutesWrapper} />
Your components are remounting every time because you're using the component prop.
Quoting from the docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means 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. When using an inline function for inline rendering, use the render or the children prop (below).
The solution you probably need in your case is to edit your Routing component to use render instead of children.

React Component does not render when clicking CardActionArea from Material UI

I'm running into a weird issue that I've never run into before.
I'm using Material UI components, specifically CardActionArea paired with
Redirect from react-router-dom
Upon clicking the CardActionArea I want to redirect my users to a detail screen of the component they just clicked.
The detail view sometimes renders and sometimes it doesn't. For example, if I click on the CardActionArea the detail view does not render, but if I navigate directly to the URL, the detail view does render.
This is the relevant code:
// Dashboard.js
return (
<Grid container spacing={40} className={classes.root}>
<TopMenu></TopMenu>
<Router>
<Route exact path="/dashboard/v/:videoId" component={VideoDetail} />
</Router>
<Router>
<Route exact path="/dashboard" component={(YouTubeVideoGallery)} />
</Router>
</Grid>
);
The CardActionArea is here:
constructor(props) {
super(props);
this.state = {
redirect: false
};
this.handleCardActionClick = this.handleCardActionClick.bind(this);
}
handleCardActionClick = () => {
this.setState({redirect: true});
}
render() {
const { classes } = this.props;
const date = moment(this.props.video.publishedAt);
if (this.state.redirect) {
return (<Redirect to={`/dashboard/v/${this.props.video.id}`} />)
}
return (
<Card className={classes.card}>
<CardActionArea onClick={this.handleCardActionClick}>
<CardHeader
title={
(this.props.video.title.length > 21) ?
this.props.video.title.substr(0, 18) + '...' :
this.props.video.title
}
subheader={`${date.format('MMMM DD[,] YYYY')} - Views: ${this.props.video.viewCount}`}
/>
<CardMedia
className={classes.media}
image={this.props.video.thumbnails.medium.url}
title={this.props.video.title}
/>
</CardActionArea>
</Card>
);
}
I'm not really sure what the problem is.
First thing handleCardActionClick is an arrow function so you no need to do binding in constructor. That can be removed
To redirect on onclick do something like below
render(){
const url = `/dashboard/v/${this.props.video.id}`;
return(
<div>
{this.state.redirect && <Redirect to={url} />}
</div>
)
}

React Router causes Redux container components re-render unneccessary

Here is my major code, App component is connected to Redux's store:
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostList} />
<Route path="/login" component={Login} />
<Route path="/topics" component={PostList} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
requestQuantity: getRequestQuantity(state)
};
};
export default connect(mapStateToProps)(App);
PostList component is also connected to Redux's store:
class PostList extends Component {
componentDidMount() {
this.props.fetchAllPosts();
}
render() {
const { posts} = this.props;
return (
// ...
);
}
//...
}
const mapStateToProps = (state, props) => {
return {
posts: getPostList(state),
};
};
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators(postActions, dispatch),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
When this.props.fetchAllPosts() is called, the requestQuantity in the global state will change from 0 to 1 (request starts) then to 0 (request ends). So the App will re-render twice. However, every re-rendering of App also causes PostList to re-render, which is what I don't expect, since PostList only depends on posts in the global state and posts doesn't change in these twice re-rendering.
I check React Router's source code and find the Route's componentWillReceiveProps will always call the setState, which set a new match object:
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
)
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
)
//the official always set a new match object ignoring whether the nextProps change or not
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
})
}
It is the new match prop passed to the PostList causing the Redux's shallow comparison fails and re-rendering occurs. I hope React Router's team can do some easy logic before setState, such as using (===) comparing every prop in nextProps and this.props, if no change occurs, skip setState. Unfortunately,they think it is not a big deal and closed my issue.
Now my solution is creating a HOC :
// connectRoute.js
export default function connectRoute(WrappedComponent) {
return class extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.location !== this.props.location;
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Then use connectRoute to wrap the containers used in Route:
const PostListWrapper = connectRoute(PostList);
const LoginWrapper = connectRoute(Login);
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostListWrapper} />
<Route path="/login" component={LoginWrapper} />
<Route path="/topics" component={PostListWrapper} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
Besides, when React Router is used with Mobx, this issue is also easy to meet.
Hope someone could offer better solutions. A long question. Thanks for your patience.

Resources