React: What happens on consecutive renders where a list shuffles? - javascript

I am trying to create a list which shuffles randomly with some animation.
Here is the fiddle for it where I have used key prop to identify each child.
http://jsfiddle.net/1wcpLLg4/
var ListAnimate = React.createClass({
list: [
{id: 1, caption: "Hello"},
{id: 2, caption: "There"},
{id: 3, caption: "Whatsup"},
{id: 4, caption: "Sanket"},
{id: 5, caption: "Sahu"},
],
shuffle: function() {
this.list.shuffle(); // Shuffles array!
this.forceUpdate();
},
render: function() {
return <div>
<button onClick={this.shuffle}>Shuffle</button>
<ul>
{this.list.map(function(el, i){
return <li key={el.id} style={ {top: (i*60)+'px'} }>{el.caption} {el.id}</li>;
})}
</ul>
</div>;
}
});
React.render(<ListAnimate />, document.body);
From the React docs about Dynamic Children, it states that key prop is used in order to identify the elements in an array during successive renders. So, that the items which are just re-ordered must not unmount and mount to new position rather they should just be re-positioned but the does not seem to be happening in the fiddle, where the Nodes at the top of the list are always being unmounted and being mounted at a different position.
But for the elements at the bottom seems to be working well with animation.

Keep in mind that for the kind of animation you are looking for, you need to always keep the DOM in the same order and only update their position in the render function of your component.
I modified your first fiddle using this strategy: http://jsfiddle.net/0maphg47/1/
render: function() {
// create a sorted version of the list to render the DOM
var sortedCopy = this.state.list.slice().sort(function(a, b) {
return a.id - b.id;
});
return <div>
<button onClick={this.shuffle}>Shuffle</button>
<ul>
{sortedCopy.map(function(el, i) {
// find the position of the element in the shuffled list
// which gives the position the element must be
var pos = this.state.list.indexOf(el);
return <li key={el.id} style={ {top: (pos*60)+'px'} }>
{el.caption} {el.id}
</li>;
}, this)}
</ul>
</div>;
}
There is still room for improvement but I'll leave that up to you.

I have created a fiddle to show that li elements are not actually being mounted / unmounted.
http://jsfiddle.net/jq9p7hnd/
I have converted li element to MyLi element and logged messages when the componentDidMount and componentWillUnmount functions are called. Only the componentDidMount callbacks are called during the first render and none of them are called after shuffle:
var MyLi = React.createClass({
componentDidMount : function(){
console.log("MyLi component did mount.");
},
componentWillUnmount : function(){
console.log("MyLi component will unmount.");
},
render : function(){
return <li {...this.props}>{this.props.children}</li>
}
});
var ListAnimate = React.createClass({
list: [
{id: 1, caption: "Hello"},
{id: 2, caption: "There"},
{id: 3, caption: "Whatsup"},
{id: 4, caption: "Sanket"},
{id: 5, caption: "Sahu"},
],
shuffle: function() {
this.list.shuffle();
this.forceUpdate();
},
render: function() {
return <div>
<button onClick={this.shuffle}>Shuffle</button>
<ul>
{this.list.map(function(el, i){
return <MyLi key={el.id} style={ {top: (i*60)+'px'} }>{el.caption} {el.id}</MyLi>;
})}
</ul>
</div>;
}
});
window.React = React;
React.render(<ListAnimate />, document.body);
Array.prototype.shuffle = function() {
var i = this.length, j, temp;
if ( i == 0 ) return this;
while ( --i ) {
j = Math.floor( Math.random() * ( i + 1 ) );
temp = this[i];
this[i] = this[j];
this[j] = temp;
}
return this;
}

Related

React - Array Splice Trouble

Learning React. Attempting to make my own mini-app based very closely on what's done here: https://www.youtube.com/watch?v=-AbaV3nrw6E.
I'm having a problem with the deletion of comments in my app. I've looked in several other places for people having similar errors, but it seems the problem is within my own code (and yet I can find no errors). I've scoured the Babel file over and over, but to no avail.
Here are the specifics:
When you create a new comment, you have two options in the form of buttons: Save and Delete. After exactly one comment is written and you press "Save," the delete function works just fine. However, if there are three comments total (for example) and you click "delete" on the first one, the next comment (the second, in this case) is deleted.
Hopefully that makes some amount of sense.
Can you find my error? The math/logic behind the delete function is located on line 71 under the name "deleteComment."
Full Pen here.
var CommentSection = React.createClass({
getInitialState: function() {
return {editing: true}
},
edit: function() {
this.setState({editing: true});
},
save: function() {
this.props.updateCommentText(this.refs.newText.value, this.props.index);
this.setState({editing: false});
},
delete: function() {
this.props.deleteFromCard(this.props.index);
},
renderNormal: function() {
return (
<div className="comment-section">
<div className="comment-content">{this.props.children}</div>
<a className="-edit" onClick={this.edit}>Edit</a>
</div>
);
},
renderEdit: function() {
return (
<div className="comment-section">
<textarea ref="newText" defaultValue={this.props.children}></textarea>
<button className="-save" onClick={this.save}>Save</button>
<button className="-delete" onClick={this.delete}>Delete</button>
</div>
);
},
render: function() {
if(this.state.editing) {
return this.renderEdit();
} else {
return this.renderNormal();
}
}
});
var PhotoSection = React.createClass({
render: function() {
return <div className="photo-section"></div>;
}
});
var Desk = React.createClass({
getInitialState: function() {
return {
comments: []
}
},
addComment: function(text) {
var arr = this.state.comments;
arr.push(text);
this.setState({comments: arr})
},
deleteComment: function(i) {
console.log(i);
var arr = this.state.comments;
arr.splice(i, 1);
this.setState({comments: arr})
},
updateComment: function(newText, i) {
var arr = this.state.comments;
arr[i] = newText;
this.setState({comments: arr})
},
commentFormat: function(text, i) {
return (
<CommentSection key={i} index={i} updateCommentText={this.updateComment} deleteFromCard={this.deleteComment}>
{text}
</CommentSection>
);
},
render: function() {
return (
<div className="desk">
<div className="card">
<PhotoSection />
<div className="comment-section-backing">
<button className="-comment" onClick={this.addComment.bind(null, "")}>Leave a Comment</button>
{this.state.comments.map(this.commentFormat)}
</div>
</div>
</div>
);
}
});
ReactDOM.render(<Desk />, document.getElementById('app'));
Your problem stems from using the index as keys:
https://facebook.github.io/react/docs/lists-and-keys.html#keys
When you delete an item from your array, the array in the state is correctly updated. However, when the array is rendered, they keys will all be the same regardless of which element you deleted except there will be one less.
At this point the reconciliation happens and your components are rerendered. However you have an (uncontrolled) textarea in each component that holds its own internal state. The uncontrolled textarea component does get it's default (initial) value from the children prop but is otherwise unaffected by changes to that value. Therefore the re-rendering of the components with new values for text do not change the values in those textarea instances.
If the keys for the components in the mapped components were not linked to the index, the correct component would be removed.
edit: The code in the pen has changed slightly where there are two different render branches (editing, normal). Since the normal rendering doesn't use the uncontrolled textarea inputs the pen no longer exhibits the aberrant behavior.
There was an issue with using this.props.children when rendering the CommentSection component
Changing the code to use a prop:
return (
<div className="comment-section">
<div className="comment-content">{this.props.commentText}</div>
<a className="-edit" onClick={this.edit}>Edit</a>
<button className="-delete" onClick={this.delete}>Delete</button>
</div>
);
and setting this in the commentFormat functiion in the container:
commentFormat: function(text, i) {
return (
<CommentSection
key={i}
index={i}
updateCommentText={this.updateComment}
deleteFromCard={this.deleteComment}
commentText={text}>
</CommentSection>
);
}
appears to work.
CodePen
Try with Array.filter.
deleteComment: function(i) {
var arr = this.state.comments.filter(function(comment) {
return comment.index !== i;
});
this.setState({comments: arr});
},

React Dynamic Child Component Numbering

I have recently started to learn react and maybe i do not fully understand how it should work.
I have created a react script
var Parent = React.createClass({
getInitialState: function() {
return {children: []};
},
onClick: function() {
var childrens = this.state.children;
childrens.push({
name: this.props.name,
index: this.state.children.length + 1,
key: this.props.name + this.state.children.length + 1
});
this.setState({children: childrens});
},
onChildMinus: function(index) {
var childrens = this.state.children;
childrens.splice(index - 1, 1);
this.setState({children: childrens});
},
render: function() {
return (
<div>
<div className="parent" onClick={this.onClick}>
{this.props.name}
- Click Me
</div>
{this.state.children.map((child) => (<Child name={child.name} index={child.index} key={child.key} onMinusClick={this.onChildMinus}/>))}
</div>
);
}
});
var Child = React.createClass({
getInitialState: function() {
return {selected: false};
},
onClick: function() {
this.setState({selected: true});
},
onMinusClick: function() {
if (typeof this.props.onMinusClick === 'function') {
this.props.onMinusClick(this.props.index);
}
},
render: function() {
let classes = classNames({'child': true, 'selected': this.state.selected});
return (
<div className={classes}>
<span onClick={this.onClick}>{this.props.name} {this.props.index}</span>
<span onClick={this.onMinusClick}>Remove</span>
</div>
)
}
});
ReactDOM.render(
<Parent name="test"/>, document.querySelector("#container"));
https://jsfiddle.net/uqcxo1pg/1/
It is a button that when you click it, it creates a child element that has a number, there is a delete button on the child element.
When you delete the child element it remove it from the parent array, but how do it make it so that it updates all of the child elements to now have the correct number?
Because you're setting the index of the child in onClick, that value is never update when a prior child is removed. If the purpose of index on <Child/> is just for numbering, you can pass the index of the child in the array instead of the index assigned in onClick. If you need both the original index and the order, I'd suggest adding another prop to <Child />.
{this.state.children.map((child, index) => (
<Child
name={child.name}
index={index}
key={child.key}
onMinusClick={this.onChildMinus}
/>
))}
https://jsfiddle.net/uqcxo1pg/2/
Update
Alternatively, if you need child.index to be updated, you'll have to iterate over this.state.children and renumber them. The most efficient way would be to start at the index of the removed child but this is the brute force alternative.
const renumberedChildren = this.state.children.map((child, index) => {
child.index = index + 1;
return child;
});

React - Tree Component

I am trying to make a component in React to recursively display the names in a data tree. I am not that familiar with React and am not sure what I can do in my code to remove the error I get in the console.
The error is Uncaught SyntaxError: embedded: Adjacent JSX elements must be wrapped in an enclosing tag
Here is a jsFiddle to my code: https://jsfiddle.net/go79b0dp/
Here is my example code:
var treeObj = [
{
id: 1,
name: 'Bob',
children: [
{
id: 2,
name: 'Mary',
children: [
{id: 4, name: 'Suzy'}
]
},
{
id: 3,
name: 'Phil',
children: [
{id: 5, name: 'Jon'},
{id: 6, name: 'Paul'}
]
}
]
}
];
var TreeView = React.createClass({
getInitialState: function() {
return {
people: treeObj
};
},
render: function() {
var people = this.state.people;
var nodes = people.map((i) => <TreeNode node={i} children= {i.children} />)
return (
<ul>{nodes}</ul>
);
}
});
var TreeNode = React.createClass({
render: function() {
var nodes;
if (this.props.children) {
nodes = this.props.children.map((i) => <TreeNode node={i} children={i.children} />);
}
return (
<li>{this.props.node.name}</li>
<ul>{nodes}</ul>
);
}
});
ReactDOM.render(<TreeView />, document.getElementById('container'));
Your TreeNode component returns two sibling components: <li> and <ul>. Basically, you're trying to return two things from the render function, and you can't do that.
Normally, the recommended solution is to wrap them both in another element. For example:
return (<div>
<li>{this.props.node.name}</li>
<ul>{nodes}</ul>
</div>);
However, for the tree structure you're trying to create, it would probably be better to put the <ul> inside the <li>. That would be:
return (<li>
{this.props.node.name}
<ul>{nodes}</ul>
</li>);
This is how nested lists are commonly done in HTML.

React: How to know which component calls the function

I am doing my first project using React and there is one thing I can't figure out. So I have many different Type components which are being set as the main component's TypesPage state. And when the onChange event happens on Type component I want to know which type it is in a TypesPage state or what index it is in a types array, so I can reupdate my data state.
Inside handleChange function I used jQuery's grep function comparing clicked Type title value with all the types array, but I am sure that is not the right way to do it and it would be an overkill with huge arrays.
Why I want to know which
handleChange:function(element, event){
var typeIndex;
$.grep(types, function(e, index){
if(element.title === e.title){
typeIndex = index
}
});
types[typeIndex] //Now I know that this is the Type that was changed
}
Fiddle
var types = [
{
type_id: 1,
type_name: "Logo"
},
{
type_id: 2,
type_name: "Ad"
},
{
type_id: 3,
type_name: "Catalog"
},
];
var Type = React.createClass({
render: function() {
return(
<li>
<input type="text" value={this.props.title}
onChange={this.props.handleChange.bind(null, this.props)} />
</li>
);
}
});
var TypesContainer = React.createClass({
render: function() {
var that = this;
return(
<ul>
{this.props.data.map(function(entry){
return(
<Type
key={entry.type_id}
title={entry.type_name}
handleChange={that.props.handleChange}
/>
);
})}
</ul>
);
}
});
var TypesPage = React.createClass({
getInitialState: function(){
return({data: types})
},
handleChange: function(element, event){
},
render: function() {
return(
<TypesContainer
data={this.state.data}
handleChange={this.handleChange}
/>
);
}
});
ReactDOM.render(
<TypesPage />,
document.getElementById('container')
);
I prefer ES6. The problem is, you have to bind your handleChange event with correct context of this and pass your arguments which you are expect to get inside your handle. See example below
class Example extends React.Component {
constructor(){
super();
this.state = {
data: [{id: 1, type: 'Hello'},{id: 2, type: 'World'},{id: 3, type: 'it"s me'}],
focusOn: null
};
}
change(index,e){
const oldData = this.state.data;
oldData[index].type = e.target.value;
this.setState({data:oldData, focusOn: index})
}
render(){
const list = this.state.data.map((item,index) =>
// this is the way how to get focused element
<input key={item.id} value={item.type} onChange={this.change.bind(this, index)}/>
);
return <div>
{list}
<p>Focused Element with index: {this.state.focusOn}</p>
</div>
}
}
React.render(<Example />, document.getElementById('container'));
fiddle
Thanks

Where is this variable coming from in this react script

I'm starting out looking at react, I've seen a good example here https://scotch.io/tutorials/learning-react-getting-started-and-concepts
however I'm not sure where 'item' is coming from, it doesn't appear to be declared anywhere. I've highlighted item in the code below.
/** #jsx React.DOM */
var List = React.createClass({
render: function(){
return (
<ul>
{
this.props.items.map(function(**item**) {
return <li key=**{item}>{item}**</li>
})
}
</ul>
)
}
});
var FilteredList = React.createClass({
filterList: function(event){
var updatedList = this.state.initialItems;
updatedList = updatedList.filter(function(**item**){
return **item**.toLowerCase().search(event.target.value.toLowerCase()) !== -1;
});
this.setState({items: updatedList});
},
getInitialState: function(){
return {
initialItems: [
"Apples",
"Broccoli",
"Chicken",
"Duck",
"Eggs",
"Fish",
"Granola",
"Hash Browns"
],
items: []
}
},
componentWillMount: function(){
this.setState({items: this.state.initialItems})
},
render: function(){
return (
<div className="filter-list">
<input type="text" placeholder="Search" onChange={this.filterList}/>
<List items={this.state.items}/>
</div>
);
}
});
React.renderComponent(<FilteredList/>, document.getElementById('mount-point'));
updatedList is an array.
Array.filter takes a function as an argument that will be passed in a variable. They are using item as that variable name.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

Categories

Resources