When Changing the length of an array passed through props, the 'shouldComponentUpdate' function can't detect the array length change.
I know that 'shouldComponentUpdate' can't detect changes in nested objects properties, but this is a simple array length!! is this a bug in React??
https://jsfiddle.net/ashraffayad/cLz1q8sv/
var ArrTest = React.createClass({
render: function() {
return <div >{this.props.arr}< /div>;
},
shouldComponentUpdate: function(nextProps) {
console.log(this.props.arr.length, nextProps.arr.length); // same length !!!
return true;
}
});
// - - - - app component
var App = React.createClass({
getInitialState: function() {
return {
arr: [1, 2, 3, 4]
};
},
render: function() {
return <ArrTest arr={ this.state.arr } />;
},
componentDidMount: function() {
var self = this;
setTimeout(function() {
self.state.arr.push(7);
self.setState(self.state);
}, 2000);
}
});
ReactDOM.render( < App /> ,
document.getElementById('container')
);
It's not a bug in React, it's an issue with your code.
You should never modify this.state values directly.
Try this:
componentDidMount: function() {
var self = this;
setTimeout(function() {
self.setState({arr: self.state.arr.concat([7])});
}, 2000);
}
It works. Because React doesn't clone props as it passes them down, and so changes to an array get reflect on all of its references.
I suggest you read more about immutability in Javascript.
In short, never do this.state.[anything].push/pop/shift/unshift(), never.
Do something like this instead:
var arr = this.state.arr.slice(); // Create a copy of the array
arr.push(2); // do whatever you want to do
this.setState({ arr: arr }); // pass changes to React
Just because you have two references (this.props.arr, nextProps.arr) does not mean you have two instances.
When you mutate the array with push, you modify the instance. When shouldComponentUpdate runs it compares the references and because they point to the same instance, the array lengths are the same.
If you want to pass down a new array with different elements or properties, then you need to create a new array too.
It's quite easy to substitute push for concat.
setTimeout(function() {
self.setState({
arr: self.state.concat([7])
}, 2000);
You're referencing the same array in your if, ie., you are modifying the same array instead of creating a new one and you're working two references to the same array in shouldComponentUpdate.
You should always treat props and state as immutable and therefore creating a new array with .concat instead of pushing onto the array in state will fix your current issue.
setTimeout(function () {
this.setState({arr: this.state.concat([7])});
}.bind(this), 2000);
If you'd have done this.props.arr === nextProps.arr within shouldComponentUpdate you'd see that the arrays would be equal to each other.
Related
For performance reason and to avoid Warning: Each child in an array or iterator should have a unique "key" prop key should be added to state.
It is unclear if key attribute should be added to DOM. For example should I add key to React.createElement('li', {className: 'book'}, in:
var books = [
{key: 1, author: "aaa", title: "111"},
];
var BookView = createReactClass({
render: function() {
return React.createElement('li', {className: 'book'},
this.props.author,
" - ",
React.createElement('i', null, this.props.title))
},
});
var BooksView = createReactClass({
render: function () {
return React.createElement('ul', {className: 'books'},
this.props.data.map(function(__) { return React.createElement(BookView, __); }))
}
});
var booksArea = document.getElementById('booksArea');
ReactDOM.render(React.createElement(BooksView, {data: books}), booksArea);
As descirbed in React Documentation :
A good rule of thumb is that elements inside the map() call need keys.
So in your case its BooksView as you are creating a lists of elements using .map()
As you are already passing key property as a props to the model , that would be sufficient to satisfy the warning.
You need to pass a key prop to the repeated element. In this case, BookView.
Since your model already has a key property, and you are passing that model as the props object, that should be sufficient.
The polymer documentation says to always use the polymer array mutation functions when manipulating arrays. I do not see a function to clear an array. I see pop, push, shift, unshift and splice. For now i using this method:
<script>
Polymer({
is: "wc-example",
properties: {
data: { type: Array, value: function () { return [1, 2, 3]; } }
},
ready: function () {
this.data = [];
}
});
</script>
This works but it doesn't seem right because i'm not using the array mutation functions. Does anyone know the correct solution?
Thank you!
This perfectly ok. You are assigning a new instance to the property and this will be tracked by Polymer. Only manipulations on the same instance need to be done using the Polymer API. Note that you could use splice to clear an array.
this.splice("data", 0, this.data.length)
I'm learning Reactjs and tried to copy the example from this facebook git, children to make the warning disappear, but I'm still getting it:
var MyComponent = React.createClass({
getInitialState: function () {
return {idx: 0};
},
render: function() {
var results = this.props.results;
return (
<ol>
{results.map(function(result) {
return <li key={result.id}>{result.book}</li>;
})}
</ol>
);
}
});
var result = [
{title: "book", content: "booky"},
{title: "pen", content: "penny"},
];
document.addEventListener("DOMContentLoaded", function () {
ReactDOM.render(<MyComponent results={results} />, document.getElementById('widgets'));
});
A few things going on here that need to be rectified:
The variable you're declaring and assigning the array of results to is named result but you are passing a variable named results as the results prop to MyComponent. Rename this variable to results.
The two object properties you're attempting to access on the result object inside the map function are id and book, neither of which are defined on the object itself (title and content are). Change {result.book} to {result.title} in this case.
Finally, in order to supply a unique ID to each element returned from the map, set the second parameter of your map (e.g. results.map(function(result, i) {... and use the array index as your unique key.
In summary:
results.map(function(result, i) {
return <li key={i}>{result.title}</li>;
})
I'm trying to figure out the best way of updating a stored array when a new item is added to it.
Right now I'm sorting it with the code below (sortMissions() is called in the AppDispatcher.register code), but it feels inefficient since the array will be sorted every time one of my switch cases is called, even if it's irrelevant to the _missions array (e.g. if _incomplete_missions changes, I'll still be sorting _missions).
//MissionStore.js
// Define initial data points
var _missions = [], _incomplete_missions = [];
// Add a mission to _missions
function addMission(mission){
_missions.push(mission);
}
// Sort _missions as desired
function sortMissions(){
_missions = _(_missions).chain().sortBy('name').sortBy('points').value();
}
...
var MissionStore = _.extend({}, EventEmitter.prototype, {
...
// Return Mission data
getMissions: function() {
return _missions;
},
// emit change event
emitChange: function() {
this.emit('change');
},
// add change listener
addChangeListener: function(callback) {
this.on('change', callback);
},
// remove change listener
removeChangeListener: function(callback) {
this.removeListener('change', callback);
}
});
AppDispatcher.register(function(payload) {
var action = payload.action;
var text;
switch(action.actionType) {
case MissionActionConstants.MISSION_SAVE:
addMission(action.mission);
break;
default:
return true;
}
sortMissions();
MissionStore.emitChange();
return true;
});
I thought about just sorting _missions in MissionStore.getMissions() and no where else, but that will result in sorting it every time getMissions() called, whether anything changed or not.
I also thought about inserting sortMissions() in every dispatcher case that _missions would change, but that seems like I'm duplicating myself.
Ideally I'd like to subscribe to changes just on the _missions array (from within the same store) and sort _missions only when it changes, but I'm not sure how I would do that.
Thanks!
Maybe you should do the sorting in the controller-view, not in the store.
This way, you maintain only one collection of missions, and this could be immutable data. I recommend ImmutableJS for that.
Then, if MissionsStore.getMissions() !== this.state.missions, you do the sort and then pass the sorted collection to this.setState().
Otherwise, I think you're looking at maintaining a separate cached collection for every type of sort, which seems like a lot to maintain. But it's certainly a viable alternative.
I want to edit my collection using jeditable, where modifyCollection is a function associated with the event dblclick. I have the following code:
initialize : function(options) {
view.__super__.initialize.apply(this, arguments);
this.collection = this.options.collection;
this.render();
},
render : function() {
var template = _.template(tpl, {
collectionForTemplate : this.collection ,
});
this.el.html(template);
return this;
},
modifyCollection : function (event){
$('#name').editable(function(value, settings) {
return (value);
}
,
{ onblur: function(value) {
this.modelID=event.target.nameID;
this.collection = this.options.collection;
console.log("This Collection is: " + this.collection); //Shows : undefined
//
this.reset(value);
$(this).html(value);
return (value);
}
});
The idee is to update the model and subsequently, the collection by means of jeditable. The in place editing works fine, but the problem is, I am not able to pass the collection into the function. I want to save all the changes to my collection locally and send them to the server at a later time. What am I doing wrong here?
Moved the comment to a formal answer in case other people find this thread.
The this inside your onblur() function is not pointing to this collection. Try adding var self = this; inside your modifyCollection() function then in your onblur() change this.collection to self.collection like so:
modifyCollection : function (event) {
var self = this; // Added this line
// When working with functions within functions, we need
// to be careful of what this actually points to.
$('#name').editable(function(value, settings) {
return (value);
}, {
onblur: function(value) {
// Since modelID and collection are part of the larger Backbone object,
// we refer to it through the self var we initialized.
self.modelID = event.target.nameID;
self.collection = self.options.collection;
// Self, declared outside of the function refers to the collection
console.log("This Collection is: " + self.collection);
self.reset(value);
// NOTICE: here we use this instead of self...
$(this).html(value); // this correctly refers to the jQuery element $('#name')
return (value);
}
});
});
UPDATE - Foreboding Note on self
#muistooshort makes a good mention that self is actually a property of window so if you don't declare the var self = this; in your code, you'll be referring to a window obj. Can be aggravating if you're not sure why self seems to exist but doesn't seem to work.
Common use of this kind of coding tends to favor using that or _this instead of self. You have been warned. ;-)