React Router causing component remount on Firebase update - reactjs

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.

Related

React Router remounts component everytime

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.

What is the simplest way to pass state while using React Router?

What is the simplest way to pass state while using React Router? My Navi component below is reflecting user being null, as opposed to user being "KungLoad". Thanks.
class App extends Component{
constructor() {
super();
this.state = {user: "KungLoad"};
}
render () {
return(
<div>
<Router>
<Route exact path="/" state component = {Navi} />
</Router>
The simplest way is that you can pass the state as props and then use it in the specified component. For your case, you have to use render instead of component for passing the state as props.
<Route exact path="/" render={() => <Navi user={this.state.user} />} />
This will work but I would recommend to you that the Context API concept of reactJS would be best suited here. You can pass the state or props to all the component using the data provider and all the components will consume the state or props that are being provided by the parent component. . https://reactjs.org/docs/context.html
version 6 react-router-dom
I know the question got answered but I feel this might be helpful example for those who want to use functional components and they are in search of passing data between components using react-router-dom v6.
Let's suppose we have two functional components, first component A, second component B. The component A wants to share data to component B.
usage of hooks: (useLocation,useNavigate)
import {Link, useNavigate} from 'react-router-dom';
function ComponentA(props) {
const navigate = useNavigate();
const toComponentB=()=>{
navigate('/componentB',{state:{id:1,name:'sabaoon'}});
}
return (
<>
<div> <a onClick={()=>{toComponentB()}}>Component B<a/></div>
</>
);
}
export default ComponentA;
Now we will get the data in Component B.
import {useLocation} from 'react-router-dom';
function ComponentB() {
const location = useLocation();
return (
<>
<div>{location.state.name}</div>
</>
)
}
export default ComponentB;
Note: you can use HOC if you are using class components as hooks won't work in class components.
Yiu can pass your state as props to your Navi component like this: <Route exact path="/" render={() => <Navi user={this.state.user} />} />
The other answers are correct, you should pass state down to children components via props. I am adding my answer to highlight one additional way that the Route component can be used. The code looks cleaner and is easier to read if you simply add children to a Route component, rather than use the render or component prop.
class App extends Component {
constructor(props) {
super(props);
this.state = {
user: "KungLoad"
};
}
render() {
return (
<div>
<Router>
<Route exact path="/">
<Navi user={this.state.user} />
</Route>
</Router>
</div>
);
}
}
After making the state and assigning value
this.state = {user: "KungLoad"};
Passing the state value to the router is done like this.
<Router>
<Route exact path="/" render={()=> (<Navi user={this.state.user}/>)} />
</Router>
Or if you want to user is not logged in use a redirect
<Route exact path="/signin" render={()=> (<Redirect to='/signin'/>)}/>

how to use react route to not rerender the entire app?

I am using an App Component to hold all the route so it looks like
class App extends React.Component {
state = { signedIn: false };
handleSignedIn = () => {
this.setState({ signedIn: true });
};
render() {
<Router>
<Route>Header</Route>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/login" component={Login}/>
{...otherRoutes}
</Switch>
<Route>Footer</Route>
</Router>;
}
}
then in one of the routes, I have something like this
<NavLink to="/">
Home
</NavLink>
However, i find that clicking on this NavLink rerenders App and set signedIn to false, what's the right/better way to navigate around this.
-- updated routes for more details
Basically i am expecting the <App> itself not to rerender, but the <Router> should.
Returning to App.js means signedIn is reset to false, since the default state is false. Refreshing the page will also rerender App.js and reset it back to false.
You need some way of checking if the user is logged in already. One way of doing so is by storing a token client-side after logging in, and checking if the token is present when a component loads.
You can possibly store the token in localStorage, and then whenever App.js mounts do something like componentDidMount -> checkToken() -> setState({signedIn: true})

How do I pass state through React_router?

Here is the file that's causing me trouble:
var Routers = React.createClass({
getInitialState: function(){
return{
userName: "",
relatives: []
}
},
userLoggedIn: function(userName, relatives){
this.setState({
userName: userName,
relatives: relatives,
})
},
render: function() {
return (
<Router history={browserHistory}>
<Route path="/" userLoggedIn={this.userLoggedIn} component={LogIn}/>
<Route path="feed" relatives={this.state.relatives} userName={this.state.userName} component={Feed}/>
</Router>
);
}
});
I am trying to pass the new this.state.relatives and this.state.userName through the routes into my "feed"-component. But I'm getting this error message:
Warning: [react-router] You cannot change ; it will be
ignored
I know why this happens, but don't know how else i'm supposed to pass the states to my "feed"-component. I've been trying to fix this problem for the past 5 hours and í'm getting quite desperate!
Please help!
Thanks
SOLUTION:
The answers below were helpful and i thank the athors, but they were not the easiest way to do this.
The best way to do it in my case turned out to be this:
When you change routes you just attach a message to it like this:
browserHistory.push({pathname: '/pathname', state: {message: "hello, im a passed message!"}});
or if you do it through a link:
<Link
to={{
pathname: '/pathname',
state: { message: 'hello, im a passed message!' }
}}/>
source: https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/location.md
In the component you are trying to reach you can then access the variable like this for example:
componentDidMount: function() {
var recievedMessage = this.props.location.state.message
},
tl;dr your best bet is to use a store like redux or mobx when managing state that needs to be accessible throughout your application. Those libraries allow your components to connect to/observe the state and be kept up to date of any state changes.
What is a <Route>?
The reason that you cannot pass props through <Route> components is that they are not real components in the sense that they do not render anything. Instead, they are used to build a route configuration object.
That means that this:
<Router history={browserHistory}>
<Route path='/' component={App}>
<Route path='foo' component={Foo} />
</Route>
</Router>
is equivalent to this:
<Router history={browserHistory} routes={{
path: '/',
component: App,
childRoutes: [
{
path: 'foo',
component: Foo
}
]
}} />
The routes are only evaluated on the initial mount, which is why you cannot pass new props to them.
Static Props
If you have some static props that you want to pass to your store, you can create your own higher order component that will inject them into the store. Unfortunately, this only works for static props because, as stated above, the <Route>s are only evaluated once.
function withProps(Component, props) {
return function(matchProps) {
return <Component {...props} {...matchProps} />
}
}
class MyApp extends React.Component {
render() {
return (
<Router history={browserHistory}>
<Route path='/' component={App}>
<Route path='foo' component={withProps(Foo, { test: 'ing' })} />
</Route>
</Router>
)
}
}
Using location.state
location.state is a convenient way to pass state between components when you are navigating. It has one major downside, however, which is that the state only exists when navigating within your application. If a user follows a link to your website, there will be no state attached to the location.
Using A Store
So how do you pass data to your route's components? A common way is to use a store like redux or mobx. With redux, you can connect your component to the store using a higher order component. Then, when your route's component (which is really the HOC with your route component as its child) renders, it can grab up to date information from the store.
const Foo = (props) => (
<div>{props.username}</div>
)
function mapStateToProps(state) {
return {
value: state.username
};
}
export default connect(mapStateToProps)(Foo)
I am not particularly familiar with mobx, but from my understanding it can be even easier to setup. Using redux, mobx, or one of the other state management is a great way to pass state throughout your application.
Note: You can stop reading here. Below are plausible examples for passing state, but you should probably just use a store library.
Without A Store
What if you don't want to use a store? Are you out of luck? No, but you have to use an experimental feature of React: the context. In order to use the context, one of your parent components has to explicitly define a getChildContext method as well as a childContextTypes object. Any child component that wants to access these values through the context would then need to define a contextTypes object (similar to propTypes).
class MyApp extends React.Component {
getChildContext() {
return {
username: this.state.username
}
}
}
MyApp.childContextTypes = {
username: React.PropTypes.object
}
const Foo = (props, context) => (
<div>{context.username}</div>
)
Foo.contextTypes = {
username: React.PropTypes.object
}
You could even write your own higher order component that automatically injects the context values as props of your <Route> components. This would be something of a "poor man's store". You could get it to work, but most likely less efficiently and with more bugs than using one of the aforementioned store libraries.
What about React.cloneElement?
There is another way to provide props to a <Route>'s component, but it only works one level at a time. Essentially, when React Router is rendering components based on the current route, it creates an element for the most deeply nested matched <Route> first. It then passes that element as the children prop when creating an element for the next most deeply nested <Route>. That means that in the render method of the second component, you can use React.cloneElement to clone the existing children element and add additional props to it.
const Bar = (props) => (
<div>These are my props: {JSON.stringify(props)}</div>
)
const Foo = (props) => (
<div>
This is my child: {
props.children && React.cloneElement(props.children, { username: props.username })
}
</div>
)
This is of course tedious, especially if you were to need to pass this information through multiple levels of <Route> components. You would also need to manage your state within your base <Route> component (i.e. <Route path='/' component={Base}>) because you wouldn't have a way to inject the state from parent components of the <Router>.
I know this is a late answer, but you can do it this way:
export default class Routes extends Component {
constructor(props) {
super(props);
this.state = { config: 'http://localhost' };
}
render() {
return (
<div>
<BrowserRouter>
<Switch>
<Route path="/" exact component={App} />
<Route path="/lectures" exact
render={() => <Lectures config={this.state.config} />} />
</Switch>
</BrowserRouter>
</div>
);
}
}
This way, you can reach config props inside the Lecture component.
This is a little walk around the issue but it is a nice start.
Just a heads up that if you're using a query string you need to add search.
For example:
{
key: 'ac3df4', // not with HashHistory!
pathname: '/somewhere',
search: '?some=search-string',
hash: '#howdy',
state: {
[userDefined]: true
}
}
It took like 20 minutes to figure out why my route was not being rendered 😅
You can not change the state of the React-router once the router component is mounted. You can write your own HTML5 route component and listen for the url changes.
class MyRouter extend React.component {
constructor(props,context){
super(props,context);
this._registerHashEvent = this._registerHashEvent.bind(this)
}
_registerHashEvent() {
window.addEventListener("hashchange", this._hashHandler.bind(this), false);
}
...
...
render(){
return ( <div> <RouteComponent /> </div>)
}
For those like me,
You can also normally render this:
import {
BrowserRouter as Router,
Router,
Link,
Switch
} from 'react-router-dom'
<Router>
<Switch>
<Link to='profile'>Profile</Link>
<Route path='profile'>
<Profile data={this.state.username} />
</Route>
<Route component={PageNotFound} />
</Switch>
</Router>
This worked for me!

React Dynamic menu items passed down via router

Another react question here, I have a solution for my problem but to me it doesn't seem very "React" so I was hoping for another solution.
I'm using react router so the bottom of my app.js(entry point) is:
ReactDOM.render(
<Router history={hashHistory}>
<Route path="/" component={Layout}>
<IndexRoute component={Login} ></IndexRoute>
<Route path="searches" component={searches}></Route>
<Route path="notifications" component={notifications}></Route>
</Route>
</Router>
, app);
Now as you can see my overarching component is Layout so, in my mind I want to configure my (reusable) components, for example I want my Layout to pass the title of the menu items to the header component, and then if I'm for example loading a search then I might want to pass functions etc to the search component to hook into it's functionality, so I have the following in layout:
export default class Layout extends React.Component {
constructor() {
super();
}
render() {
const containerStyle = {
paddingRight: '5px'
}
// Figure out which props we want based on location.pathname
const {pathname} = this.props.location;
switch(pathname){
case "/searches":
// So now I need to add some props, functions or anything else to this component
theProps = {someProp: "value"}
const theComponent = React.cloneElement(this.props.children, {theProps})
break;
}
return (
< div style={containerStyle} class="container">
< Header menuItems={this.getMenuItemsForScreen()}/ >
{ theComponent}
< Footer / >
< /div>
);
}
}
So basically in order to pass props from my overarching Layout I have to clone the component and then give it some more props?? It just feels a bit dirty but I can't seem to find a way of embedding this type of logic otherwise?
Thanks
Marc
I think the great thing about these routing components is that they save you from those ugly switches in your components.
I'm not sure which kind of props you want to send to the searches component. In your question is not clear what is the actual problem you are trying to solve instead of using one of the standard approaches in the react-router documentation.
I recommend considering these alternatives:
Refactor your searches component to avoid receiving any props. Try to have each route to have a component that doesn't receive any props. So you move that code that define the props (theProps = {someProp: "value"}) inside the searches component. Or if you need the searches component to be reused with those props and other props in another time, then make a new parent component that defines those props and calls the searches component then. But if those props are to complex and dependent on your app state then maybe you can consider using flux, redux or another state container, and get those from the app state.
If you really need the routing parameters, then make sure the props can be serialized so they can be part of the URL. Check the message route in the code below (copied from RouteConfiguration sample):
import React from 'react'
import { render } from 'react-dom'
import { Router, Route, Link } from 'react-router'
const App = React.createClass({
render() {
return (
<div>
<h1>App</h1>
<ul>
<li><Link to="/about">About</Link></li>
<li><Link to="/inbox">Inbox</Link></li>
</ul>
{this.props.children}
</div>
)
}
})
const About = React.createClass({
render() {
return <h3>About</h3>
}
})
const Inbox = React.createClass({
render() {
return (
<div>
<h2>Inbox</h2>
{this.props.children || "Welcome to your Inbox"}
</div>
)
}
})
const Message = React.createClass({
render() {
return <h3>Message {this.props.params.id}</h3>
}
})
render((
<Router>
<Route path="/" component={App}>
<Route path="about" component={About} />
<Route path="inbox" component={Inbox}>
<Route path="messages/:id" component={Message} />
</Route>
</Route>
</Router>
), document.body)
In this case your code will have <a href={"/inbox/message/"+id} ...> somewhere in your code and those will provide the props by setting the id parameter in this case.
you use functional component in child component with this code :
<Route path="/:id" component={Child} />
function Child({ match }) {
return (
<div>
<h2>ID:{match.params.id}</h2>
</div>
);
}

Resources