Endless loop rendering component on ReactJs - javascript

I'm facing an infinite loop issue and I can't see what is triggering it. It seems to happen while rendering the components.
I have three components, organised like this :
TimelineComponent
|--PostComponent
|--UserPopover
TimelineComponenet:
React.createClass({
mixins: [
Reflux.listenTo(TimelineStore, 'onChange'),
],
getInitialState: function() {
return {
posts: [],
}
},
componentWillMount: function(){
Actions.getPostsTimeline();
},
render: function(){
return (
<div className="timeline">
{this.renderPosts()}
</div>
);
},
renderPosts: function (){
return this.state.posts.map(function(post){
return (
<PostComponenet key={post.id} post={post} />
);
});
},
onChange: function(event, posts) {
this.setState({posts: posts});
}
});
PostComponent:
React.createClass({
...
render: function() {
return (
...
<UserPopover userId= {this.props.post.user_id}/>
...
);
}
});
UserPopover:
module.exports = React.createClass({
mixins: [
Reflux.listenTo(UsersStore, 'onChange'),
],
getInitialState: function() {
return {
user: null
};
},
componentWillMount: function(){
Actions.getUser(this.props.userId);
},
render: function() {
return (this.state.user? this.renderContent() : null);
},
renderContent: function(){
console.log(i++);
return (
<div>
<img src={this.state.user.thumbnail} />
<span>{this.state.user.name}</span>
<span>{this.state.user.last_name}</span>
...
</div>
);
},
onChange: function() {
this.setState({
user: UsersStore.findUser(this.props.userId)
});
}
});
Finally, there is also UsersStore**:
module.exports = Reflux.createStore({
listenables: [Actions],
users: [],
getUser: function(userId){
return Api.get(url/userId)
.then(function(json){
this.users.push(json);
this.triggerChange();
}.bind(this));
},
findUser: function(userId) {
var user = _.findWhere(this.users, {'id': userId});
if(user){
return user;
}else{
this.getUser(userId);
return [];
}
},
triggerChange: function() {
this.trigger('change', this.users);
}
});
Everything works properly except the UserPopover component.
For each PostComponent is rendering one UserPopOver which fetch the data in the willMount cycle.
The thing is, if you noticed I have this line of code console.log(i++); in the UserPopover component, that increments over and over
...
3820
3821
3822
3823
3824
3825
...
Clearl an infinite loop, but I really don't know where it comes from. If anyone could give me a hint I will be very gratefully.
PS: I already tried this approach in the UsersStore but then all the PostComponent have the same "user":
...
getUser: function(userId){
return Api.get(url/userId)
.then(function(json){
this.user = json;
this.triggerChange();
}.bind(this));
},
triggerChange: function() {
this.trigger('change', this.user);
}
...
And in the UserPopover
...
onChange: function(event, user) {
this.setState({
user: user
});
}
...

Because that your posts is fetch async, I believe that when your UserPopover component execute it's componentWillMount, the props.userId is undefined, and then you call UsersStore.findUser(this.props.userId), In UserStore, the getUser is called because it can't find user in local storage.
NOTE that every time the getUser's ajax finished, it trigger. So the UserPopover component execute onChange function, and call UsersStore.findUser again. That's a endless loop.
Please add a console.log(this.props.userId) in the UserPopover's componentWillMount to find out if it is like what i said above. I actually not 100% sure it.
That is a problem that all UserPopover instance share the same UserStore, I think we should rethink the structure of these components and stores. But I haven't thought out the best way yet.

You can do it like this:
TimelineComponent
|--PostComponent
|--UserPopover
UserPopover just listen for changes and update itself.
UserPopover listens for change at store, which holds which user's data should be in popover and on change updates itself. You can send also coordinates where to render. No need to create Popover for each Post.

Related

Vue screen that refreshes periodically, done safely

I have a page in Vue/Nuxt that needs to refresh a list of items every few seconds. This is an SPA that does an Axios fetch to a server to get updated information. At the moment, I have something like this:
methods: {
doRefresh() {
setTimeout(function() {
// trigger server fetch here
doRefresh();
}, 5000);
}
}
It works, unless the other code in doRefresh throws an error, in which case the refreshing stops, or somehow the code gets called twice, and I get two timers going at the same time.
An alternative is call setInterval() only once. The trouble with that is that it keeps going even after I leave the page. I could store the reference returned by the setInterval(), and then stop it in a destroyed() hook. But again, an error might prevent that from happening.
Is there a safe and reliable way to run a timer on a Vue page, and destroy it when the user leaves the page?
This approach together with try-catch is a way to go, have a look at this snippet:
https://codepen.io/alexbrohshtut/pen/YzXjNeB
<div id="app">
<wrapper/>
</div>
Vue.component("interval-component", {
template: `
<div> {{lastRefreshed}}
<button #click="init">Start</button></div>`,
data() {
return {
timeoutId: undefined,
lastRefreshed: undefined
};
},
methods: {
doJob() {
if (Math.random() > 0.9) throw new Error();
this.lastRefreshed = new Date();
console.log("Job done");
},
init() {
if (this.timeoutId) return;
this.run();
},
run() {
console.log("cycle started");
const vm = this;
this.timeoutId = setTimeout(function() {
try {
vm.doJob();
} catch (e) {
console.log(e);
} finally {
vm.run();
}
}, 2000);
}
},
destroyed() {
clearTimeout(this.timeoutId);
console.log("Destroyed");
}
});
Vue.component("wrapper", {
template: `<div> <button #click="create" v-if="destroyed"> Create</button>
<button v-else #click="destroy">Destroy</button>
<interval-component v-if="!destroyed" /></div>`,
data() {
return {
destroyed: true
};
},
methods: {
destroy() {
this.destroyed = true;
},
create() {
this.destroyed = false;
}
}
});
new Vue({
el: "#app"
});

How does react.js know that data has changed?

Using React.js I have written a simple app that gets json and uses some of that data returned to build html.
Although, when the JSON changes, the html does not. Am I missing something here?
Here is my code -
<script type="text/jsx">
var classNames = ({
'auditNumber': "auditNumber",
'audit-overview-box': "audit-overview-box"
});
var AuditOverviewBox = React.createClass({
render: function () {
return (
<div className="audit-overview-box">
<h1 className={classNames.auditNumber}>{this.props.auditNo}</h1>
<span>{this.props.creationDate}</span>
</div>
)
}
});
var AuditBoxes = React.createClass({
getInitialState: function () {
return {
data: []
}
},
componentWillMount: function () {
this.dataSource();
},
componentWillReceiveProps: function (nextProps) {
this.state.data(nextProps);
},
dataSource: function (props) {
props = props || this.props;
return $.ajax({
url: '../json.php',
dataType: 'json',
cache: false,
success: function (data) {
this.setState({data: data});
}.bind(this),
error: function (xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
render: function () {
var data = this.state.data;
console.log("data");
var photos = data.map(function (audit) {
return <AuditOverviewBox key={audit.auditNo.toString()} auditNo={audit.auditNo}
creationDate={audit.creationDate}/>
});
return (
<div className='myAudits'>
{photos}
</div>
)
}
});
ReactDOM.render(<AuditBoxes />, document.getElementById('audits-div'));
</script>
And the JSON -
[{
"auditNo": "a1201",
"creationDate": "21/10/2016"
},
{
"auditNo": "a1221",
"creationDate": "21/10/2016"
},
{
"auditNo": "a1211",
"creationDate": "21/10/2016"
}]
You cannot push changes from the server to the browser (unless you use websockets). If you just need to update once in a while you should setup your code around the ajax request in such a way that it will perform a request every n seconds. The simplest solution would be using setInterval()
setInterval(
function () {
// your ajax code
},
5000
)
that way the request to the server will be done every 5 seconds. Please be aware that you can overload your server if you set the interval to short / have a lot of visitors.
There are only two ways to change the data. You can use .setState method or directly set data to .state property and call .forceUpdate method of component, but this method is stritly unrecommended.
You can read more about it here: https://facebook.github.io/react/docs/state-and-lifecycle.html

Throwing "unexpected token" error, when i use jsop api in reactjs?

When i used to fetch data from json api its throwing "Unexpected token" error. Below, i've added my code what i have tried so far. Get me out from this issue. I'm trying to solve this problem long time.
Here,
var Demo = React.createClass({
render: function() {
getInitialState:function(){
return {
data:[]
};
},
componentDidMount: function () {
$.ajax({
url: "http://www.w3schools.com/angular/customers.php"
}).done(function(data) {
this.setState({data: data})
});
},
return (
<div>
{this.props.data.map(function(el,i) {
return <div key={i}>
<div>{el.Name}</div>
<div>{el.City}</div>
<div>{el.Country}</div>
</div>;
}
</div>
);
}
});
var Stream = React.createClass({
render: function() {
return (
<div>
<div className="scrollContent ">
<Demo />
</div>
</div>
);
}
});
You have several errors in your code
move getInitialState and componentDidMount from render method, these methods should be as children of your component (Demo) class but not as children of render method
add dataType: 'json' to $.ajax, because now it returns string, but in your case you need get json
as you are using this.setState in .done you should set this to .done callback, because now this refers to $.ajax object not Demo, you can use .bind method to do it.
change this.props.data to this.state.data because data located in state object not in props
array with data located in records property use it instead of just data
Example
var Demo = React.createClass({
getInitialState:function() {
return {
data :[]
};
},
componentDidMount: function () {
$.ajax({
url: "http://www.w3schools.com/angular/customers.php",
dataType: 'json'
}).done(function(response) {
this.setState({ data: response.records });
}.bind(this));
},
render: function() {
var customers = this.state.data.map(function(el,i) {
return <div key={i}>
<div>{el.Name}</div>
<div>{el.City}</div>
<div>{el.Country}</div>
</div>
});
return <div>{ customers }</div>;
}
});

ReactJs error - Warning: setState(...): Can only update a mounted or mounting component

I am getting this error can anyone please tell me how I can debug this further?
Warning: setState(...): Can only update a mounted or mounting
component. This usually means you called setState() on an unmounted
component. This is a no-op.
Can anyone help?
This is my component which is causing the error:
var postal = require('postal'),
contactChannel = postal.channel("contact"),
React = require('react');
var ContactSelector = React.createClass({
getInitialState: function() {
return {
selectedContacts:[]
};
},
handleChange: function(e) {
var id = e.target.attributes['data-ref'].value;
if (e.target.checked === true){
contactChannel.publish({
channel: "contact",
topic: "selectedContact",
data: {
id: id
}});
} else{
contactChannel.publish({
channel: "contact",
topic: "deselectedContact",
data: {
id: id
}
});
}
},
render: function() {
var id = this.props.data.id;
var isSelected = this.props.data.IsSelected;
return (
<div className="contact-selector">
<input type="checkbox"
checked={isSelected} data-ref={id}
onChange={this.handleChange} />
</div>
);
}
});
module.exports = ContactSelector;
The contactChannel is a channel I've setup using postal.js, https://github.com/postaljs/postal.js
contactChannel.subscribe("selectedContact",function (data, envelope) {
page.setPersonIsSelectedState(data.id, true);
basketChannel.publish({
channel: "basket",
topic: "addPersonToBasket",
data: {
personId: data.id
}
});
});
I suscribe to the publish in componentDidMount on my parent page:
componentDidMount: function() {
var page = this;
this.loadContacts();
page.subscribeEvents();
},
Listeners:
subscribeEvents: function() {
var page = this;
page.subscribeToChannel(filterChannel, "searchFilterChange", this.listenerForSearchFilterChanged);
contactChannel.subscribe("pageSizeChanged", this.listenerForSizeChanged);
page.subscribeToChannel(filterChannel, "genderFilterChange", this.listnerForGenderFilterChange);
page.subscribeToChannel(filterChannel, "rollModeFilterChange", this.listnerForRollModeFilterChange);
page.subscribeToChannel(filterChannel, "attendanceModeFilterChange", this.listnerForAttendanceModeFilterChange)
page.subscribeToChannel(filterChannel, "messageToFilterChange", this.listnerForMessageToFilterChange);
contactChannel.subscribe("selectAll", function (data) {
page.loadContacts();
});
contactChannel.subscribe("selectedContact",function (data, envelope) {
page.setPersonIsSelectedState(data.id, true);
basketChannel.publish({
channel: "basket",
topic: "addPersonToBasket",
data: {
personId: data.id
}
});
});
contactChannel.subscribe("selectAll", function (data, envelope) {
basketChannel.publish({
channel: "basket",
topic: "selectAll",
data: {
selectAll: data.selectAll
}
});
});
contactChannel.subscribe("refreshContacts", function (data, envelope) {
page.loadContacts();
});
},
Add a ref attribute to your root div, and check that ref value before calling setState . This will make sure the component is mounted.
render: function() {
var id = this.props.data.id;
var isSelected = this.props.data.IsSelected;
return (
<div ref='some_ref' className="contact-selector">
<input type="checkbox"
checked={isSelected} data-ref={id}
onChange={this.handleChange} />
</div>
);
}
then call setState like below
this.refs.some_ref ? this.setState({yourState:value}): null;
You're going about using react incorrectly. React is built to be componentized, so you'll want to be doing everything in components.
When you're setting up your app, you'll want to use postal's subscribe inside of each of your components' getInitialState. Then, unsubscribe from the postal channels in the componentWillUnmount functions.
It seems that the offending code is missing from the snippets in your question, if you post all your code on I could look at it and tell you specifically where you are still "subscribed" to a postal event on a component that is no longer mounted.

Router doesn't wait for subscription

My problem is that I have two similar paths and in first one router waits for my subscriptions and renders whole template, but the second one is rendering right away with no loading and data passed is causing errors(since there is no collection subscribed yet).
I paste my code here, the second one is different because of template and data passed but the rest is practically the same.
I'm just starting with iron-routing, maybe someone can tell me where is mistake?
Router.map(function() {
this.route('/', {
onBeforeAction: function() {
if (Meteor.user()) {
if (Meteor.user().firstLogin)
this.render("firstLogin");
else
Router.go('/news');
} else {
this.render("start");
}
},
waitOn: function() {
return Meteor.subscribe('allUsers');
},
onAfterAction: function() {
document.title = "someTitle";
},
loadingTemplate: "loading",
});
this.route('users',{
path:'/user/:_id',
layoutTemplate: 'secondLayout',
yieldTemplates: {
'template1': {to: 'center' },
'template2': {to: 'top' },
'template3': {to: 'left' },
'template4': {to: 'right' },
},
waitOn: function(){
return Meteor.subscribe("allUsers");
},
data: function(){
return Meteor.users.findOne({_id:String(this.params._id)});
},
loadingTemplate: "loading",
});
});
You are using iron-router in the lagacy. If you're just starting it. I recommend you use the new api. In that case, you can use this.ready() to check the subscription is finished or not
Following is the example from the official guide
Router.route('/post/:_id', function () {
// add the subscription handle to our waitlist
this.wait(Meteor.subscribe('item', this.params._id));
// this.ready() is true if all items in the wait list are ready
if (this.ready()) {
this.render();
} else {
this.render('Loading');
}
});

Categories