So I'm working on a react app using, among other things, react-router. I'm having a problem where I'm changing the route, the change is reflected in the URL, but the mounted component doesn't change. Here is the component, it's one of my main container components:
class AppContent extends Component {
state = {
isStarted: false
};
componentDidMount = async () => {
try {
await this.props.checkIsScanning();
this.setState({ isStarted: true });
}
catch (ex) {
this.props.showErrorAlert(ex.message);
}
};
componentWillUpdate(nextProps) {
if (nextProps.history.location.pathname !== '/' && !nextProps.isScanning) {
this.props.history.push('/');
}
}
render() {
return (
<div>
<VideoNavbar />
{
this.state.isStarted &&
<Container>
<Row>
<Col xs={{ size: 8, offset: 2 }}>
<Alert />
</Col>
</Row>
<Switch>
<Route
path="/scanning"
exact
render={ (props) => (
<Scanning
{ ...props }
isScanning={ this.props.isScanning }
checkIsScanning={ this.props.checkIsScanning }
/>
) }
/>
<Route path="/"
exact
render={ (props) => (
<VideoListLayout
{ ...props }
isScanning={ this.props.isScanning }
/>
) }
/>
</Switch>
</Container>
}
</div>
);
}
}
const mapStateToProps = (state) => ({
isScanning: state.scanning.isScanning
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
checkIsScanning,
showErrorAlert
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(AppContent));
So, let's focus on the important stuff. There is a redux property, isScanning, that is essential to the behavior here. By default, when I open the app, the route is "/", and the VideoListLayout component displays properly. From there, I click a button to start a scan, which changes the route to "/scanning", and displays the Scanning component. The Scanning component, among other things, calls the server on an interval to check if the scan is done. When it is complete, it sets "isScanning" to false. When AppContent re-renders, if "isScanning" is false, it pushes "/" onto the history to send the user back to the main page.
Almost everything here works. The Scanning component shows up when I start the scan, and it polls the server just fine. When the scan is complete, redux is properly updated so "isScanning" now is false. The componentWillUpdate() function in AppContent works properly, and it successfully pushes "/" onto the history. The URL changes from "/scanning" to "/", so the route is being changed.
However, the Scanning component remains mounted, and the VideoListLayout component is not. I can't for the life of me figure out why this is happening. I would've thought that once the route was changed, the components would change as well.
I'm sure I'm doing something wrong here, but I can't figure out what that is. Help would be appreciated.
I'm pretty sure you're running into this issue described in the react-router docs where react-redux's shouldComponentUpdate keeps your component from re-rendering on route change: https://reacttraining.com/react-router/core/guides/redux-integration/blocked-updates. This can definitely be a pain and pretty confusing!
Generally, React Router and Redux work just fine
together. Occasionally though, an app can have a component that
doesn’t update when the location changes (child routes or active nav
links don’t update).This happens if: The component is connected to
redux via connect()(Comp). The component is not a “route component”,
meaning it is not rendered like so: The problem is that Redux implements
shouldComponentUpdate and there’s no indication that anything has
changed if it isn’t receiving props from the router. This is
straightforward to fix. Find where you connect your component and wrap
it in withRouter.
So in your case, you just need to swap the order of connect and withRouter:
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContent));
Related
I've got a React app with URLs defined with React Router:
const App: React.FC = (): JSX.Element => {
return (
<Router>
<Switch>
<Redirect exact from="/" to="/rules" />
<Route
exact
path={["/rules", "/rules/:placeId"]}
component={LandingPage}
/>
<Route
exact
path={["/route", "/route/:start/:end"]}
component={RoutePage}
/>
</Switch>
</Router>
);
};
I want to allow the user to go to a specific accordion of the document if the url contains a hash part like this /rules/someplace#accordion-3
I have a component that returns the accordions in question, which all have an id:
const CategoryDisplay: React.FC<Props> = (props) => {
...
return (
<>
<Accordion
id={`category-${accordion.id}`}/>
</>
);
};
export default CategoryDisplay;
And when I open an URL with an anchor like /rules/someplace#accordion-3 while I'm already on the page, everything works fine and it scrolls to the element. However, on the initial load of the page, this behavior doesn't work.
How would I go about scrolling to the element only after the page has loaded?
I think the idea would be to use an effect to scroll to the appropriate component after the component mounts. Perhaps something like this:
React.useEffect(() => {
const anchor = window.location.hash.slice(1);
if (anchor) {
const anchorEl = document.getElementById(anchor);
if (anchorEl) {
anchorEl.scrollIntoView();
}
}
}, []);
Notes:
I haven't tested this.
useLayoutEffect instead of useEffect may give a better experience here; see this article for details.
I'm bypassing React and using the DOM. I usually try to avoid that, but since this logic is isolated and inherently focused on browser DOM behavior, I think this is a good use case for it (instead of complicating the code by pulling in more interactions with your React components.)
scrollIntoView is convenient but experimental; consider using the scroll-into-view-if-needed NPM package instead.
I don't know if I understand correctly.
I think, you should make a function for wait everything complete.
but, if you can't catch exactly timing when everything fine,
you can use trick.
example :
const CategoryDisplay: React.FC<Props> = (props) => {
...
const [accordionId, setAccordionId] = useState("");
useEffect(()=>{
// this component mounted
...
setTimeout(()=>{
// do something
setAccordionId(`category-${accordion.id}`);
},0);
}, []);
return (
<>
<Accordion
id={accordionId}/>
</>
);
};
This will definitely run in the frame after the component mounted.
I'm using react router v4, had some issue reloading the page (not window.location.reload). I better give a real use case to explain the issue, we use a social network app as the example:
user A commented a post by user B, a notification appear in user B page.
user B clicked on the notification, we do this.props.history.push('/job/' + id'), it worked, hence user B went to job/123 page.
user A commented again, new notification appear in user B page, while user B still remain on the job/123 page, he clicked on the notification link and triggered this.props.history.push('/job' + id'). But he won't see the page rerender, he DID NOT see the latest comment because the page does nothing.
It seems to be a common scenario in many cases. It can be tackled using many different approaches. Check this stackoverflow question. There are some good answers and findings. Personally this approach made more sense to me.
location.key changes every single time whenever user tries to navigate between pages, even within the same route. To test this place below block of code in you /jod/:id component:
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
console.log("... prevProps.key", prevProps.location.key)
console.log("... this.props.key", this.props.location.key)
}
}
I had this exact same situation. Updated state in componentDidUpdate. After that worked as expected. Clicking on items within the same route updates state and displays correct info.
I assume (as not sure how you're passing/updating comments in /job/:id) if you set something like this in your /job/:id component should work:
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
this.setState({
comments: (((this.props || {}).location || {}).comments || {})
})
}
}
You are describing 2 different kinds of state changes.
In the first scenario, when user B is not at the /job/:id page and he clicks a link you get a URL change, which triggers a state change in the router, and propagates that change through to your component so you can see the comment.
In the second scenario, when user B is already at the /job/:id page and a new comment comes through, the URL doesn't need to change, so clicking on a link won't change the URL and won't trigger a state change in the router, so you won't see the new content.
I would probably try something like this (pseudo code because I don't know how you're getting new comments or subscribing via the websocket):
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
class Home extends React.Component {
render() {
return (
<div>
<h1>The home page</h1>
{/* This is the link that the user sees when someone makes a new comment */}
<Link to="/job/123">See the new comment!</Link>
</div>
);
}
}
class Job extends React.Component {
state = { comments: [] };
fetchComments() {
// Fetch the comments for this job from the server,
// using the id from the URL.
fetchTheComments(this.props.params.id, comments => {
this.setState({ comments });
});
}
componentDidMount() {
// Fetch the comments once when we first mount.
this.fetchComments();
// Setup a listener (websocket) to listen for more comments. When
// we get a notification, re-fetch the comments.
listenForNotifications(() => {
this.fetchComments();
});
}
render() {
return (
<div>
<h1>Job {this.props.params.id}</h1>
<ul>
{this.state.comments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
</div>
);
}
}
ReactDOM.render(
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/job/:id" component={Job} />
</Switch>
</BrowserRouter>,
document.getElementById("app")
);
Now the page will get updated in both scenarios.
this solved the problem for me :
import { BrowserRouter as Router,Route,Switch,Redirect} from "react-router-dom";
<Router>
<Switch>
<Redirect from="/x_walls/:user_Id" to='/walls/:user_Id'/>
<Route path="/walls/:user_Id" exact render={(props) => <Wall {...props}/> }/>
</Switch>
</Router>
and when you want to call "walls" you just call "x_walls" instead
You should watch updating postId in componentWillUpdate life cycle and do something from here like this:
componentWillUpdate(np) {
const { match } = this.props;
const prevPostId = match.params.postId;
const nextPostId = np.match.params.postId;
if(nextPostId && prevPostId !== nextPostId){
// do something
}
}
My React Router thingy looks like this:
<Switch>
<Route path='/posts/:id' component={PostsView} />
<Route exact path='/' component={Home}/>
<Route component={NotFound}/>
</Switch>
I would now like to implement an AudioPlayer and show it on the same page.
I could of course just import the AudioPlayer in the PostsView and render it from there. However, I think it would be nicer and more pure to not mix the two components, but have them next to another in a third component which renders both of them and is only responsible for showing stuff on the screen.
I tried to do that, but I got stuck, since my Posts Component needs the route /posts/:id, so I could not just put it in a third component. I tried something like this:
<Switch>
<Route path='/sentences/:id' component={Main} />
<Route exact path='/' component={Home}/>
<Route component={NotFound}/>
</Switch>
and then in Main I tried:
render() {
return (
<div>
<PostsView />
<AudioPlayer />
</div>
);
}
... but this seems to be the wrong approach, since things go wrong with react router then! What would be the most sensible approach to combine the two components, and keep my routing intact?
If the audioplayer is something that is shown globally, and plays something that's not always relevant to what's being displayed currently in the app, you might want to consider moving that something to a global store like redux. When you select something to be played, you would update redux with an action, and inside AudioPlayer, you would listen for changes in the store and adjust accordingly.
import { setCurrentlyPlaying } from "actions"
class SomeComponent extends React.Component {
onClickTrack = (track) => {
this.props.setCurrentlyPlaying(track)
}
}
export default connect(null, { setCurrentlyPlaying })(SomeComponent)
this random component sets the currently playing track via redux - you can also adjust your action to, say, add to a playlist, etc.
In audioplayer
class AudioPlayer extends React.Component {
componentWillReceiveProps(nextProps) {
const nextTrack = nextProps.track
const currentTrack = this.props.track
if (nextTrack !== currentTrack) {
// do something
}
}
shouldComponentUpdate(nextProps) {
const nextTrack = nextProps.track
const currentTrack = this.props.track
if (nextTrack !== currentTrack) {
// do something
}
}
render() {
return (<audio src={this.props.track.src}/>)
}
}
export default connect(
(state) => {
return { track: state.currentlyPlaying }
}
)(AudioPlayer)
you can do it a number of ways. I'm not sure how the audio element will react to re-renders (I've never used the element myself), so you might have to implement shouldComponentUpdate and make sure you're not updating the component unless you need to.
Doing this, you would have a global "container" which would display AudioPlayer and React Router.
<div>
<Switch>...</Switch>
<AudioPlayer/>
</div>
edit, you don't need redux if you either a) don't mind passing down a callback to set the track to be played and maintaining this track in a common parent.
class Parent extends React.Component {
state = { currentlyPlaying: null }
setCurrentlyPlaying = (track) => {
this.setState({ currentlyPlaying: track })
}
render() {
return (
<div>
<Child setCurrentlyPlaying={this.setCurrentlyPlaying}/>
<AudioPlayer currentlyPlaying={this.state.currentlyPlaying}/>
</div>
)
}
}
or b) use context to provide a callback - i would not recommend doing it this way as even React's maintainers recommend against using context. I'm not as familiar with context, so if you want to do it this way, you'll need to look up how to do it.
I am getting the error
Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
I found the cause to be
const mapStateToProps = (state) => {
return {
notifications: state.get("notifications").get("notifications").toJS()
}
}
If I do not return notifications there it works. But why is that?
import {connect} from "react-redux"
import {removeNotification, deactivateNotification} from "./actions"
import Notifications from "./Notifications.jsx"
const mapStateToProps = (state) => {
return {
notifications: state.get("notifications").get("notifications").toJS()
}
}
const mapDispatchToProps = (dispatch) => {
return {
closeNotification: (notification) => {
dispatch(deactivateNotification(notification.id))
setTimeout(() => dispatch(removeNotification(notification.id)), 2000)
}
}
}
const NotificationsBotBot = connect(mapStateToProps, mapDispatchToProps)(Notifications)
export default NotificationsBotBot
import React from "react"
class Notifications extends React.Component {
render() {
return (
<div></div>
)
}
}
export default Notifications
UPDATE
On further debugging I found that, the above may not be the root cause after all, I can have the notifications stay but I need to remove dispatch(push("/domains")) my redirect.
This is how I login:
export function doLogin (username, password) {
return function (dispatch) {
dispatch(loginRequest())
console.log("Simulated login with", username, password)
setTimeout(() => {
dispatch(loginSuccess(`PLACEHOLDER_TOKEN${Date.now()}`))
dispatch(addNotification({
children: "Successfully logged in",
type: "accept",
timeout: 2000,
action: "Ok"
}))
dispatch(push("/domains"))
}, 1000)
}
}
I find that the dispatch causes the warning, but why? My domains page have nothing much currently:
import {connect} from "react-redux"
import DomainsIndex from "./DomainsIndex.jsx"
export default connect()(DomainsIndex)
DomainsIndex
export default class DomainsIndex extends React.Component {
render() {
return (
<div>
<h1>Domains</h1>
</div>
)
}
}
UPDATE 2
My App.jsx. <Notifications /> is what displays the notifications
<Provider store={store}>
<ConnectedRouter history={history}>
<Layout>
<Panel>
<Switch>
<Route path="/auth" />
<Route component={TopBar} />
</Switch>
<Switch>
<Route exact path="/" component={Index} />
<Route path="/auth/login" component={LoginBotBot} />
<AuthenticatedRoute exact path="/domains" component={DomainsPage} />
<AuthenticatedRoute exact path="/domain/:id" component={DomainPage} />
<Route component={Http404} />
</Switch>
<Notifications />
</Panel>
</Layout>
</ConnectedRouter>
</Provider>
Your dispatch(push('/domains')) comes along other dispatches that set the state for a connected component (presumably one that cares about notifications) that gets remounted/unmounted after the push takes effect.
As a workaround and proof of concept, try defering the dispatch(push('/domains')) call with a nested zero-second setTimeout. This should execute the push after any of the other actions finish (i.e. hopefully a render):
setTimeout(() => dispatch(push('/domains')), 0)
If that works, then you might want to reconsider your component structure. I suppose Notifications is a component you want to mount once and keep it there for the lifetime of the application. Try to avoid remounting it by placing it higher in the component tree, and making it a PureComponent (here are the docs). Also, if the complexity of your application increases, you should consider using a library to handle async functionality like redux-saga.
Even though this warning usually appears because of a misplaced action call (e.g. calling an action on render: onClick={action()} instead of passing as a lambda: onClick={() => action()}), if your components look like you've mentioned (just rendering a div), then that is not the cause of the problem.
I had this issue in the past, and managed to resolve it using redux-batched-actions.
It's very useful for use-cases like yours when you dispatch multiples actions at once and you're unsure of when the updates will get triggered, with this, there will be only one single dispatch for multiple actions. In your case it seems that the subsequent addNotification is fine, but the third one is too much, maybe because it interacts with the history api.
I would try to do something like (assuming your setTimeout will be replaced by an api call of course):
import { batchActions } from 'redux-batched-actions'
export function doLogin (username, password) {
return function (dispatch) {
dispatch(loginRequest())
console.log("Simulated login with", username, password)
setTimeout(() => {
dispatch(batchActions([
loginSuccess(`PLACEHOLDER_TOKEN${Date.now()}`),
addNotification({
children: "Successfully logged in",
type: "accept",
timeout: 2000,
action: "Ok"
}),
push("/domains")
]))
}, 1000)
}
}
Note that you'll have to download the package and enableBatching your reducer in your store creation.
The reason this is happening is when are you calling the doLogin, if you are calling it from within a constructor. If this is the case try moving it to componentWillMount although you should be calling this method from a button or enter hit in the login form.
This have been documented in constructor should not mutate If this is not the root of the problem you mind commented each line in doLogin to know exactly which line giving the state problem, my guess would be either the push or the addNotification
There is not enough info to give a certain answer. But what is for sure is that this warning is raised when you tries to setState inside render method.
Most often it happens when you are calling your handler functions instead of passing them as props to child Component. Like it happened here or here.
So my advice is to double check which Components are being rendered on your /domains route and how you are passing onChange, onClick, etc. handlers to them and to their children.
In a react component when you call setState({...}), it causes the component to re-render and call all the life cycle methods which are involved in re-rendering of the component and render method is one of them
When in render if you call setState({...}) it will cause a re-render and render will be called again and again hence this will trigger an infinite loop inside the javascript eventually leading to crashing of the app
Hence, not only in render but in any life-cycle method which is a part of re-render process setState({...}) method shouldn't be called.
In your case the code might be triggering an update in the redux state while the code is still rendering and hence this causes re-render and react shows error
I'm taking my first steps with react-router.
I'm currently using the hashHistory for development purposes and I'm performing 'manual' navigation. That is to say, I'm not using Link and I'm invoking history.push('/some/route'); in order to navigate (in response to plain old clicks on anchor tags).
What I'm noticing is that, even when I'm already on the target route, react-router will re-render the relevant target component every time history.push('/target/route'); is invoked: On every push('/target/route'):
the fragment part of the URL remains #/target/route
the query string part of the URL changes to ?_k=somethingRandom
the target component re-renders
I would like for that re-rendering to not happen - I actually expected history.push to be a no-op when I'm already at the route that I'm attempting to push.
I'm apparently missing something, as this is not what's happening. Funnily enough I'm seeing posts from people who are trying to achieve the behaviour that I'd like to get rid of - they'd like to 'refresh' a route without leaving it, so to speak. Which looks pretty much like the opposite problem :).
Could you enlighten me as to what it is I'm misunderstanding and how I would achieve the desired behaviour? Is this perhaps something that would go away if (when) I switch to browserHistory?
My guess is that your component re-renders because something in your prop changes when you make a router push. I suspect it might be the action or key properties of prop.location. You could always check all the values of prop during each render to see what changes.
You can solve this issue by comparing your old route path with the new one in the shouldComponentUpdate life-cycle method. If it hasn't changed you are on the same route, and you can prevent the re-rendering by returning false. In all other cases, return true. By default this always returns true.
shouldComponentUpdate: function(nextProps, nextState) {
if(this.props.route.path == nextProps.route.path) return false;
return true;
}
You'll have to make further checks as well as this will prevent your component from updating on state updates within the component as well, but I guess this would be your starting point.
Read more about shouldComponentUpdate on the official react docs page.
Use this as an opportunity to return false when you're certain that the transition to the new props and state will not require a component update.
I have the same issue and i find the (dumb) solution.
You just have a <button> (button by default is type=submit) or something similar (form, submit.... etc) thats is reloading the page like a html <form method=GET ...>.
Check it in your code, and remove it.
PD:
_k=somethingRandom > this is just the value inputs (or the button) that you are sending in the form.
I will give this a shot...
If you land here and looking to change your URL (for sharing purposes for example) then RR docs already has the solution described. Just make sure you do not use the history within the component (i.e. this.props.history.push())as you will be (as expected) routed to the target. You are however allowed to access your browser history without any interference with the component's history.
Following tested only on Chrome
// history.js
import { createBrowserHistory } from 'history'
export default createBrowserHistory()
and then from your XYZ component
// XYZ.js
import React from 'react';
import history from './history'
class XYZ extends React.Component {
_handleClick() {
// this should not cause rerender and still have URL change
history.push("/someloc");
}
render() {
return(
<button onClick={this._handleClick.bind(this)}>test </button>
)
}
}
Hope it helps someone else.
In App.js:
shouldComponentUpdate(nextProps, nextState) {
return nextProps.location.search === this.props.location.search
}
I think the easier workaround maybe replacing the Route with our own route
import { Route, withRouter } from "react-router-dom";
function MyRoute({ key, path, exact, component: Component, history }) {
let lastLocation = null;
return (
<Route
key={key}
path={path}
exact={exact}
render={(props) => {
history.listen((location) => {
lastLocation = location;
});
// monkey patching to prevent pushing same url into history stack
const prevHistoryPush = history.push;
history.push = (pathname, state = {}) => {
if (
lastLocation === null ||
pathname !==
lastLocation.pathname + lastLocation.search + lastLocation.hash ||
JSON.stringify(state) !== JSON.stringify(lastLocation.state)
) {
prevHistoryPush(pathname, state);
}
};
return <Component {...props} />;
}}
/>
);
}
export default withRouter(MyRoute);
We use this as a wrapper for actual Route of react-router-dom and it works perfectly for me.
for more please refer here
tsx sample
import {createBrowserHistory} from 'history';
export const history = createBrowserHistory();
ReactDOM.render(
<Router history={history}>
<App/>
</Router>,
document.getElementById("root")
);