Infinite componentDidUpdate() calls with react-router - reactjs

I am new to react-router and right now I have following routes in my app:
<Router history={browserHistory}>
<Route path="/" component={MainLayout}>
<Route path="/v/:username/:reponame/:page/:perPage" component={Results} />
</Route>
</Router>
As you can see, there's a MainLayout component that includes an <input type="text"> which is used to connect to Github API and retrieve list of issues for a certain repo.
Then the Results component steps in. Here's the code for it's componentDidMount():
componentDidMount() {
const {
username,
reponame,
page,
perPage
} = this.props.params;
this.sendRequest(username, reponame, page, perPage);
}
sendRequests essentially contains the ajax query for fetching the output data, after which it's being set into the component's state:
this.state = {
data: [], // here
lastPage: -1,
errorMessage: ''
}
Now this works pretty well till the very moment when one wants to change the value of an input.
As I see, the Result component doesn't unmount. Instead, it invokes componentWillReceiveProps() and updates the existing component. AFAIK it is safe to perform side calls in componentDidUpdate() so I just copied the code above from componentDidMount() and pasted it in there. This way, though (and it is absolutely reasonable) componentDidMount() is being invoked over and over again.
The only workaround I came up with at the moment is comparing old and new data in the sendRequest() itself and invoke setState() inside of it only if it differs via deep-equal package, so it looks like this:
if (!equal(res, this.state.data)) {
this.setState({
...this.state,
data: res,
lastPage
});
}
Is this considered to be an ok pattern or there is a better way to solve this issue?

You should not use setState inside the cDM lifecycle. as it might trigger re-render, which will cause your infinite loop.
Updating the state after a component mount will trigger a second render() call and can lead to property/layout thrashing.
https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-mount-set-state.md

Related

componentDidMount always called before react render

I am new to react world, I tried to fetch user data from axios call, and tried to get the data before the react's render executed.
How I call this component
<Route path="/users" render={(props) => <User {...props}/>} />
Here is my component class
class User extends React.Component {
constructor(props) {
super(props);
this.state = { authenticated: false }
this.getCurrentUser = this.getCurrentUser.bind(this);
}
componentDidMount(){
console.log("componentDidMount");
this.getCurrentUser();
}
getCurrentUser(){
Api.getCurrentUser().then(response => {
if (response) {
console.log(response.data.username);
this.setState({authenticated: true});
}
}).catch(error =>{
this.setState({authenticated: false});
}
}
render() {
console.log(this.state.authenticated);
if (this.state.authenticated === false) {
return <Redirect to={{ pathname: '/login' }}/>
}
return (
<div><Page /> <div>
);
}
}
export default User;
The console.log sequence is
false
componentDidMount
user_one
Warning: Can't perform a React state update on an unmounted component. This is a no-op
The warning makes sense because react already redirect me to login so the user component is not mounted.
I don't understand why componentDidMount is not called before render, because it supposes to change the defaultState through this.setState()...
What am I missing here?
ComponentDidMount works the way you described it. It runs immediately after the component is rendered. What you can do is to wrap your Component with a parent component where you have the API call and pass on the isAuthenticated as props to .
Docs for reference
As #user2079976 rightly stated, your usage of componentDidMount is correct & it behaves the way it is intended to, but I think you might be getting the wrong impression due to your code execution workflow.
Problem Reason
This issue/warning is generally something to go with when you're updating a component that has unmounted, in your case it's likely the redirect that happens before your api return a result.
More Details:
Not having the full code sample, I had to guess a few of the variables in your setup & I'm unable to get the exact issue on my JsFiddle as you've explained (I think JsFiddle/react.prod swallows the warning messages), but... I'll try to update this fiddle to explain it as much as I can with comments.
// at render this is still false as `state.authenticated`
// only becomes true after the redirect.
// the code then redirects....
// then updates the not yet mounted component & its state
// which is causing the warning
if (this.state.authenticated === false) {
return (<Redirect to={{ pathname: '/about' }}/>)
}
return (<div>On Home</div>);
Possible Solution
Rather do your auth/logged-in (state) to a higher level/parent component, and have the router decide where to send the user.
We have used this exact example in one of our apps (which is an implementation of the above suggestion). It works well for an auth type workflow & is straight from the docs of the Router lib you're using :P https://reactrouter.com/web/example/auth-workflow

State change not re-rendering component

I couldn't find the answer anywhere because nothing was working for me, so I'm starting a new topic. Please, don't mark it as a duplicate
Router.js:
<Switch>
<Route path="/" exact component={Home} />
<Route exact path="/p/:uid" component={Profile} />
</Switch>
Profile.js
constructor(props) {
super(props);
this.state = {
loading: true,
profile_user_id: this.props.match.params.uid,
};
}
Then, later in Profile.js, I trigger a fetch to get data from backend and save this data to state using this.setState({ ... }). When the Profile component is rendered, everything looks fine.
In Router.js, there is also a Link:
<Link to={"/p/" + this.state.ntuser.tag}>Profile</Link>
.. which should go to your own profile. So when your user ID is 1, and you visit the profile of the user with id 22, your URL will be /p/user22 and the link will point to /p/user1.
The problem is that even though profiles are rendered nicely, Profile component does not become re-rendered when you click the link (which should direct you to /p/user1 and show your profile instead). I tried to save location from react-router to state as well, so every time URL changes it will be caught in componentWillReceiveProps() and inside I update state. But still nothing. Any ideas?
PS: I'm using React.Component
console.log(this.props.match.params.uid) in constructor, componentDidMount() and componentDidUpdate() (UNSAFE_componentWillReceiveProps() is deprecated)
Number and places (of log calls) will tell you if component is recreated (many construcor calls) or updated (cDM calls). Move your data fetching call accordingly (into cDM or cDU ... or sCU).
You can save common data in component above <Router/> (f.e. using context api) - but this shouldn't be required in this case.
Solution
You can update state from changed props using componentDidUpdate() or shouldComponentUpdate(). componentDidUpdate should be protected with conditions to prevent infinite loop. See docs.
Simplest solution:
componentDidUpdate(prevProps) {
if (this.props.some_var !== prevProps.some_var) {
// prop (f.e. route '.props.match.params.uid') changed
// setState() - f.e. for 'loading' conditional rendering
// call api - use setState to save fetched data
// and clearing 'loading' flag
}
}
shouldComponentUpdate() can be used for some optimalizations - minimize rerenderings.
Had same problem in my project, Didn't find any good workable idea exept making somethink like this:
import {Profile} from "../Profile "
<Link to={`your href`}
onClick={userChanged}/>
..........
function userChanged() {
const userCard= new Profile();
userCard.getProfileData();
}
First of all you need to make your Profile component as export class Profile extends React.Component (export without default).
getProfileData is method where i get data from my api and put it state. This will rerender your app
Here is what happens:
You go to /p/user22.
React renders <Route exact path="/p/:uid" component={Profile} />.
The Route renders the Profile component for the first time.
The Profile component calls the constructor, at the time, this.props.match.params.uid is equal to "user22". Therefore, this.state.profile_user_id is now "user22".
You click on the link to /p/user1.
React renders <Route exact path="/p/:uid" component={Profile} />, which is the same Route.
The Route then rerenders the same Profile component but with different props.
Since, its the same Profile component, it does not update the state.
This explains why you still see the profile of user22

Passing props through react router not available in componentDidMount?

I've passed props via the react router like this to the mentioned component:
<BrowserRouter>
<Switch>
<Route path="/product-page" exact render={(props) => ( <ShopPages {...props} response={this.state.response}/>)}/>
</Switch>
</BrowserRouter>
I would like to access this props in componentDidMount() like something like this:
componentDidMount() {
console.log(this.props.response)
}
The prop this.props.response is available in the component I can console.log it in the render(). I'm not sure why in the above scenario the console shows the array empty. I've tried to see if the data is available then show the data as so console.log( 'show', this.props.response && this.props.response) or by adding async as so:
async componentDidMount() {
const data = await this.props.response
}
But this does nothing too. Any help?
I'm just going to assume that this.state.response is not a Promise, but is actually the data since you said it was an empty array, therefore async/await will do nothing.
ComponentDidMount() is only fired once when the component mounts and is NOT the place to perform actions based on prop updates that can happen after mount. So I would suggest passing the promise, and if that is not an option do something like
componentDidUpdate(prevProps, prevState) {
if (!prevProps.response.length && this.props.response.length) {
//Your code
}
}

Reactjs route for asterisk executes only once

I have a single page app, I have defined all the Routes in the app to execute the same react component (using *-wildcard) when navigating to them.
it seems that the component will only execute once upon navigation.
How can I call an execution/instantiation of the component upon any change in navigation?
this is my Route jsx:
<Route path="/" component={App}>
{<IndexRoute component={TVPage} />}
{<Route path="*" component={TVPage} />}
</Route>
I assume when you say "the component only executes once" you mean it mounts only once.
Since you didn't show your code, I can only assume you have used one of the lifecycle methods: componentWillMount | componentDidMount
These methods only trigger once on Component mount. Given your Route configuration, whenever you switch to a different URL, since it's using the same component, it will not unmount and mount again (thus your loading logic is only triggered once), but simply re-render if its props have changed. That's why you should plug on a lifecycle method that is triggered on every prop change (like componentWillReceiveProps).
Try this instead:
class TVPage extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
// Load your data/state (initial)
this.props.loadMyData(this.props.whatever.data);
}
componentWillReceiveProps(nextProps) {
if (this.props.whatever.myStatus !== nextProps.whatever.myStatus) {
// Load your data/state (any page change)
nextProps.loadMyData(nextProps.whatever.data);
}
}
render() {
// Render whatever you want here
}
}
componentWillMount will trigger on mount (initial load), and componentWillReceiveProps will trigger at least every time your props change.
Look at this example for react router using query params : https://github.com/reactjs/react-router/blob/master/examples/query-params/app.js
In your componentDidMount function inside your TVPage component, I would get the data passed as params in the URL which then updates the state of the component. Every time the state changes within the component, it will reload itself.
Example component :
class TVPage extends Component {
constructor(props) {
super(props);
this.state = {
data: null
}
}
componentDidMount() {
// from the example path /TVPage/:id
let urlData = this.props.params.id;
this.setState({data: urlData})
}
render() {
return (
<div>
{this.state.data}
</div>
);
}
}

Why am I losing my ReactJS state?

I'm new to ReactJS, but I have a simple use case: I have a login form that sets the user's state (full name, etc.) and then I use the React Router to browserHistory.push('/') back to the home page. I can confirm that if I stay on the login page that my states actually get saved, however when I go back to the homepage, and into my "parent" component (App.js) and run this before the render method:
console.log(this.state) // this returns null
It always returns true. My constructor is not even doing anything with the state. If I put a log in the constructor on my App.js parent component I can verify that the page is not actually being reloaded, that the component is only mounted once (at least the constructor on App.js is only called once during the whole homepage > login > homepage lifecycle). Yet again, the state seems to be removed after changing pages.
What simple thing am I missing?
Edit: some code, trying to simplify it:
// LoginForm.js
//Relevant method
handleSubmit() {
this.login(this.state.username, this.state.password, (success) => {
if (!success)
{
this.setState({ isLoggedIn: false })
this.setState({ loginError: true })
return
}
this.setState({ isLoggedIn: true })
browserHistory.push('/') // I can verify it gets here and if at this point I console.log(this.isLoggedIn) I return true
})
}
// App.js
class App extends React.Component {
constructor(props) {
super(props);
console.log('hello')
}
render() {
const { props } = this
console.log(this.state) // returns null
return (
<div>
<AppBar style={{backgroundColor: '#455a64'}}
title="ADA Aware"
showMenuIconButton={false}>
<LoginBar/>
</AppBar>
<div>
{props.children}
</div>
</div>
)}
//Part of my routes.js
export default (
<Route path="/" component={App}>
<IndexRoute component={HomePage}/>
<Route path="/login" component={LoginForm}/>
<Route path="*" component={NotFoundPage}/>
</Route>
);
Where you call handleSubmit(), what component calls it?
If it is <LoginForm> or <LoginBar> or something like this, your this. means that component, non <App>.
To set parent's state (<App> in your case) you should pass to child a property with a handler function (for example onLogin={this.handleAppLogin.bind(this)}) so your child must call this prop (this.props.onLogin(true)) and handler in the <App> will set App's state: handleAppLogin(isLoggedIn) { this.setState({isLoggedIn}); }
But for the "global" state values such as login state, access tokens, usernames etc, you better shoud use Redux or some other Flux library.
This was a closed issue router project and discussed in many SO articles:
https://github.com/ReactTraining/react-router/issues/1094
https://github.com/reactjs/react-router-redux/issues/358
Redux router - how to replay state after refresh?
However, it is persistent real world need. I guess the pattern of how to handle this is based on Redux and/or browser storage.

Resources