I have a render function inside App.js.
return (
<div className="container">
<h1 className="text-center main-title">Activity Logger</h1>
<Router>
<NavigationBar />
<Route exact path="/">
{this.renderForm()}
</Route>
<Route path="/activitydisplay">{this.renderTable()}</Route>
</Router>
</div>
);
}
The Router is a BrowserRouter. The functions it call are
renderForm = () => {
if (this.state.formDataError) {
return "Error loading data";
}
console.log("renderForm was called");
return (
<div className="mt-3">
<ActivityForm
machinesList={this.state.machinesList}
operatorsList={this.state.operatorsList}
onFormSubmit={this.postFormData}
postSuccessCount={this.state.postSuccessCount}
loggedOperator={this.props.cookies.get("logger") || null}
/>
</div>
);
};
renderTable() {
if (this.state.tableDataError) {
return "Error loading data";
}
return (
<div className="mt-3">
<ActivityDisplay
activityData={this.state.activityData}
machines={this.state.machinesList}
operators={this.state.operatorsList}
editDataHandler={this.editData}
deleteDataHandler={this.deleteData}
/>
</div>
);
}
The components are remounted when I switch between the routes at the front end. To troubleshoot, I put logging in renderForm function and in the ActivityForm constructor. I can see the logging from renderForm only when the App component is mounted and not when I switch between routes. However, I can see the logging from ActivityForm constructor whenever I switch between the components at the front end using a navigation bar.
I lose all the states I had in the component because of this. The behavior I expect is that the component should not remount when I switch tabs. Can anyone please help?
React Router does basically update the UI according to a specific state. They handle that in a very organized manner with Browser Navigation events like Back, Refresh & so on.
As we all know, when React change the UI according to a specific updated state, it will mount & unMount relevant components accordingly. https://reacttraining.com/blog/mount-vs-render/ article explains that concept very nicely.
That same behavior will apply to React Router also. When you navigate to different path, existing components can be unMount & new components can be mount according to your code.
Route with render prop
return (
<div className="container">
<h1 className="text-center main-title">Activity Logger</h1>
<Router>
<NavigationBar />
<Router>
<Route path="/activitydisplay" render={() => this.renderTable()} />
<Route exact path="/" render={() => this.renderForm()} />
</Router>
</Router>
</div>
);
Note - It's better if you can pass the component into Route using render prop. That will make renderForm or renderTable to run before ActivityForm or ActivityDisplay mount. It will also make sure that to pass all the updated props correctly to the components where necessary.
Let me know if you need further support.
Related
Expected: when clicking on the image from one component(PhotosList), it routes to another page with the specified url to view the other component(Image details)
Reality: what happens that when clicking on the image, both components are rendered on the same page but with the different url.
using import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
Here's the return of the functional component(PhotosList)
return (
<Router>
<div className="layout">
<Masonry gutter={"5 px"} columnsCount={3}>
{photosList.map((photo: any) => (
<Link to={"/details"}>
<img src={photo.download_url} />
</Link>
))}
</Masonry>
<button onClick={loadMore} className="btn-grad">
{isLoading ? "Loading..." : "Load More"}
</button>
<Switch>
<Route exact path={"/details"} component={ImageDetails}></Route>
</Switch>
</div>
</Router>
);
and the app file is
const App = () => {
return (
<div className="App">
<header>Album</header>
<PhotosList />
</div>
);
};
export default App;
return (
<div className="layout">
<Masonry gutter={"5 px"} columnsCount={3}>
{photosList.map((photo: any) => (
<Link to={"/details"}>
<img src={photo.download_url} />
</Link>
))}
</Masonry>
<button onClick={loadMore} className="btn-grad">
{isLoading ? "Loading..." : "Load More"}
</button>
</div>
);
and in the app file
const App = () => {
return (
<Router>
<div className="App">
<header>Album</header>
<PhotosList />
<Switch>
<Route exact path={"/photilist"} component={PhotosList}></Route>
<Route exact path={"/details"} component={ImageDetails}></Route>
</Switch>
</div>
</Router>
);
};
export default App;
From what I can gather, you have rendered the PhotoList component which then renders a list of photos each with a link to /details.
In the same component you set up the /details route.
Whenever you click the Link which redirects to /details, the PhotoList component will (should) unmount. Once this component is unmounted, so is the Route which sets up the component which should be rendered on /details route. So you are on /details but the router has no idea what component should be rendered because the following code:
<Switch>
<Route exact path={"/details"} component={ImageDetails}></Route>
</Switch>
no longer exists.
To avoid this, don't put Routes this far down in your application. Routes should always be close to the top level (children/grandchildren of Router). This makes projects way easier to manage. I do have projects with upwards of 100 routes and I do split up my routes into seperate files (for example each module will have a ModuleRoutes.js file in which I set up the routes for that module), then you can create a RootRouter components which renders these seperate JS files (Which are really React components) and you render the RootRouter inside of your <BrowserRouter> / <Router>.
You can abstract this even higher by creating an array of objects which hold details about each route (route name, path, component for the route, function / data you want to pass down to the component, access control (true/false) defined as a logical expression of your choosing - i.e. user.isLoggedIn === true). By employing this abstraction you minimize the room for error because errors and bugs are much easier to spot when they are in this kind of form (object) as opposed to JSx which takes up more lines of code, has messy indentation and is not very clean in general.
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.
I have two different components in React "Header" and "Main".
Header:
import React, { Component } from 'react'
import Logo from './HeaderComps/Logo'
import UserHeader from './HeaderComps/UserHeader'
export default class Header extends Component {
render() {
return (
<header>
<Logo />
<UserHeader name ="Boris"/>
</header>
)
}
}
Main:
export default class Main extends Component {
state ={isLogged : false}
handleClientFirstImpact = () =>{
if(this.state.isLogged === false){
return <Switch>
<Route exact path ='/registration' component={Register} />
<Route exact path ='/login' component={Login} />
</Switch>
}
}
render() {
return (
<Router>
<div className="Main">
{this.handleClientFirstImpact()}
</div>
</Router>
)
}
}
In "Main" I have two components "Register" and "Login".
How do I make Login page effect the Header's state? is there a React technology or a way to do that?
I want the "UserHeader" to show the User name but I don't know how to effect it's parent's state.
There might be some common component where you will be calling the Main as well as the Header Component. Suppose that component is App Component.
So inside App you have
render() {
return
<div>
<Header />
<Main />
</div>
}
Now what you can do is keep the userName in this App Component's state and this App Component will pass userName to the Component as :-
<Header userName={userName} />
Also pass a function as a prop to the Main Component which will enable the component to set the State of the Parent Component.
<Main setUserName={(newUserName) => this.setState(newUserName)} />
now this setUserName prop should be passed on to the components which are called via Route inside the Main Component. Keeping your example in mind (use render prop instead of component for Route):
export default class Main extends Component {
state ={isLogged : false}
handleClientFirstImpact = () =>{
const { setUserName } =this.props;
if(this.state.isLogged === false){
return
<Switch>
<Route exact path ='/registration'
render={(props) => <Register {...props} setUserName={setUserName} />}
/>
<Route exact path ='/login'
render={(props) => <Login {...props} setUserName={setUserName} />}
/>
</Switch>
}
}
render() {
return (
<Router>
<div className="Main">
{this.handleClientFirstImpact()}
</div>
</Router>
)
}
}
Now you have passed setUserName as a prop to both login and register and you can use this method to set App component's state which will in turn reflect the changes on the Header component.
Although the solution might work for you. I would advise you to simplify the Application layout. Keep the routing functionality in the main app Component. Use a separate layout component to render similar page layouts. It would avoid confusion in the long run.
I'm trying to figure out how to change the state of my Dashboard when a new route is clicked. This new route change needs to update the TopMenu component.
This is the code for the Dashboard
class Dashboard extends Component {
constructor (props) {
super(props)
this.state = {
selectedMenuItem: 'Now'
}
}
render () {
return (
<Router>
<div id='dashboard-container'>
<LeftMenu/>
<div className='column'>
<TopMenu selectedMenuItem={this.state.selectedMenuItem} />
<div id='content-container'>
<div>
<Switch>
<Route exact path='/' component={Now} />
<Route exact path='/surveys' component={Surveys} />
<Route exact path='/my-questions' />
<Route exact path='/feedback' />
<Route exact path='/logout' />
<Route render={function () {
return <p>Not Found</p>
}} />
</Switch>
</div>
</div>
</div>
</div>
</Router>
)}}
This is the code for the TopMenu
class TopMenu extends Component {
render () {
return (
<h3>{this.props.selectedMenuItem}</h3>
)
}
}
How do I listen to a change in React Router such that I can change the 'selectedMenuItem' state variable in the Dashboard and pass that to TopMenu.
Thanks!
Assuming you are using version 4, check the usage of match in React Router 4: https://reacttraining.com/react-router/web/api/match
match will include all the path information you will need to handle route changes. You can access it by calling this.props.match in your top level page component.
I had the same problem and I solved it with withRouter which is build for integrate router with redux, the only thing you need to do is wrap your connect with withRouter. for more information read redux integration document
I have an App component which, using react-router, holds a few components in two routes. I also have a Firebase data store which I want to bind to the state of App (using rebase) so I can pass it down to any component I wish as a prop. This is my App class:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: {}
};
}
componentDidMount () {
rebase.bindToState('items', {
context: this,
state: 'items'
})
}
render() {
return (
<Router>
<div className='container'>
<div className="header">
<h3>Header</h3>
<Nav/>
</div>
<Switch>
<Route exact path='/' component={() => <Home items={this.state.items} rebase={rebase} />} />
<Route render={function () {
return <p>Not Found</p>
}} />
</Switch>
</div>
</Router>
)
}
}
Now, when I load my page I get two mounts of the Home component. This in itself is not great. However, I have several actions in the Home component that use rebase to modify/read from Firebase. As a callback of these actions they also change the Home component's state. The problem is, whenever I do a Firebase call, it remounts the Home component and any state I have is lost.
If I remove the Router wrappers from the Home component, and render it purely as render( <Home items={this.state.items} rebase={rebase} /> ), my app works perfectly as intended. I don't know why wrapping it in Router stuff makes it not work. I thought it was because I had additional URL parameters that also changed when I call firebase updates (e.g. /?p=sgergwc4), but I have a button that changes that parameter without a firebase update and it doesn't cause any problems (i.e. doesn't cause a remount). So what's up with the Router?
Turns out the answer is simple; instead of component={}, I should use render={}. Fixes everything. It was in the docs too.