I have a strange behavior. Once I add the socket connection to the react / redux system, my main component always will be rerendered, as soon as a next Action is dispatched.
I also have the behavior, when I click again on a navigation link (dispatch the same routing action),
the compoenent also rerenders, even I stay on the same page.
Could anyone help me here to get on track?
Thanks a lot!
Setup
react 0.15.x
redux
react-router v4
react-router-redux
app.jsx and App Structure for the Container:
class Root extends React.Component {
render() {
return (
<Provider store={store}>
<Router history={history}>
<Route path="/" component={RootContainer}>
<IndexRoute component={HomePage} />
<Route path="/start" component={StartPage} />
<Route path="*" component={NotFoundPage} />
</Route>
</Router>
</Provider>
);
}
}
RootContainer
class RootContainer extends React.Component {
...
componentWillMount() {
this.connectToSocket();
this.joinChannel();
}
componentWillUnmount() {
this.socket.disconnect();
}
...
connectToSocket() {
this.socket = new Socket('/socket');
this.socket.connect();
this.socket.onOpen(() => {
this.props.connectState(); // ACTION CALL
});
this.socket.onError((err) => {
this.props.disconnectState(err); // ACTION CALL
}
}
...
reducer
Something is changing the store state. It appears your RootContainer component is connected to Redux, so it is likely rerendering each time. There are two main approaches to optimizing this:
Only connect lower-level components to the state that they need. Redux's connect higher order component only rerenders when mapStateToProps produces different results. Doing this at a more granular level lower in the component tree reduces the amount of components that have to rerender on store updates.
Extend React.PureComponent for any children that aren't connected to Redux, and don't need to rerender each time their parent does. By default, when a top-level component rerenders, all of its children rerender too. PureComponent can prevent that.
This is only general advise, it's hard to say what exactly you should do without seeing more of your app's structure.
Related
When I go to one functional component using react-router, it renders twice.
However, when I refresh the page of that component, it only renders once.
For the test, created empty functional component like that:
import React from 'react'
const TestFunctional: React.FC<any> = () => {
console.log('Test===>>>') // console log twice when navigate to this component
return <></>
}
export default TestFunctional
Here is Router in App.tsx
import React from 'react'
import { Route, Switch, useLocation, withRouter } from 'react-router-dom'
import TestFunctional from 'views/Test'
const AnimatedSwitch = withRouter(({ location }) => (
<Switch>
<Route exact path="/" component={StartPage} />
<Route exact path="/test" component={TestFunctional} />
</Switch>
))
const App = () => {
return (
<div className="app">
<Web3ReactManager>
<AnimatedSwitch />
</Web3ReactManager>
</div>
)
}
export default App
I did not use React.StrictMode in index.tsx.
ReactDOM.render(
<ApolloProvider client={client}>
<Provider store={store}>
<ConnectedRouter history={history}>
<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
<App />
</Web3ProviderNetwork>
</Web3ReactProvider>
</ConnectedRouter>
</Provider>
</ApolloProvider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers
serviceWorker.unregister()
So it is some weird.
When I refresh this page, console.log('Test===>>>') show only once.
What is a mistake and how to fix the double render problem?
Why is that a problem? You should design/write your components assuming that it could re-render at anytime. React is even working on a new rendering mode where your component might be rendered multiple times before it actually gets "rendered in DOM".
As for why it actually renders twice? Not sure, might just be a quick of ReactDOM. As a side note, the documentation for component does have this warning for Route though:
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).
While that shouldn't apply in this case, still handy to know.
I'm using MobX #observer and #withRouter (react-router-v4) wrap page component like this
#withRouter
#inject('stores')
#observer
class Page extends Component {
render() {
return (
<div>
<NavBar />
<Header title={this.props.stores.UIStore.title} />
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/about" component={AboutPage} />
<Route component={NotFound} />
</Switch>
</div>
)
}
Problem
When route location change the NavBar and Header component alway re-render with same props (no any state update). react-perf show many wasted render (no props/state update) when route change.
NavBar include Link and some MobX state (NavBar wrap with #observer+#inject only)
Header is just a stateless component.
Page component require #withRouter cause of #observer (MobX) break react-router (https://github.com/mobxjs/mobx-react/issues/210)
How to prevent NavBar and Header re-render from route location change? Allow re-render only when mobx state update.
I know this is quite old, but that’s how I solved the same problem in my project:
Don’t use withRouter and observer together. Observer implementation of shouldComponentUpdate will return true if location or match objects change.
When you just need a history or location objects use routerStore from mobx-react-router (https://github.com/alisd23/mobx-react-router)
When you need match (usually because of params), make a non-observer component that uses withRouter and pass necessary params to lower levels of hierarchy.
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.
I'm trying to get the current path of the react router in a container so I can pass it to a child component that will change it's visibility filter.
More specifically, I'm trying to make a navigation menu highlight the currently active page.
I'm using react, redux, react-router, and react-router-redux so I can access the router state from the redux store.
From the docs for react-router-redux, it says to do something like this:
function mapStateToProps(state, ownProps) {
return {
id: ownProps.params.id,
filter: ownProps.location.query.filter
};
}
Here is my container component:
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import {
Segment as UISegment,
} from 'semantic-ui-react'
import NavMenu from '../components/NavMenu'
class MenuBar extends Component {
static propTypes = {
path: PropTypes.string.isRequired
}
render() {
const { path, } = this.props
return (
<UISegment>
<NavMenu activePath={path} />
</UISegment>
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
path: ownProps.route ? ownProps.route.path : "/"
}
}
export default connect(mapStateToProps)(MenuBar)
Inside the NavMenu component, a semantic-ui menu component will compare activePath with its own path and highlight the active button.
Everything seems to work in theory; when I click on the different parts of the menu, a ##router/LOCATION_CHANGE action is emitted. In the redux dev tools, I see the state changing. However, mapStateToProps is never called and this component is never re-rendered.
Any ideas? I thought about using the react methods like shouldComponentUpdate, but it seems that react doesn't even realize the state or props are changing.
First thing to note is that you are not actually accessing router state from the store. If you look at the react-router-redux docs, it actually warns against doing so
You should not read the location state directly from the Redux store. This is because React Router operates asynchronously (to handle things such as dynamically-loaded components) and your component tree may not yet be updated in sync with your Redux state. You should rely on the props passed by React Router, as they are only updated after it has processed all asynchronous code.
Your container is reading data from ownProps, which is just the props that are passed into that container component. The example in the react-router-redux docs that you are referencing only works for a top-level route component (a component that is passed as the component prop to a React Router Route component). React Router passes the router data into all route components.
In your case, MenuBar is a child of whatever your top level route component is. Your two options are to
Pass the data you want into MenuBar down from your route component.
Use React Router's withRouter higher order component to inject the values into MenuBar https://github.com/ReactTraining/react-router/blob/v3/docs/API.md#withroutercomponent-options
Also, I believe the value you are looking for is ownProps.location.pathname rather than ownProps.route.path
Some code for option 1, since I'm assuming MenuBar isn't nested too deeply in your component tree:
If your route config is
<Router history={browserHistory}>
<Route path="/" component={AppLayout}>
<Route path="about" component={About}/>
<Route path="users" component={Users}/>
<Route path="*" component={NoMatch}/>
</Route>
</Router>
your AppLayout would be something like
const AppLayout = ({ children, location }) => {
return (
<div>
<MenuBar path={ location.pathname } />
{ children }
</div>
)
}
and MenuBar would receive the data your are looking for.
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!