Why is React's DOM reconciliation not working as expected? - reactjs

I'm trying to swap two children of an element with a transition using React.
<div style={{position: 'relative'}}>
{this.state.items.map((item, index) => (
<div
key={item}
style={{position: 'absolute',
transform: `translateY(${index * 20}px)`,
transition: '1s linear transform'}}>
{item}
</div>
))}
</div>
state.items is an array of two items. When it is reordered, the two child divs should transition to their new positions.
What happens in reality is while the second element transitions as expected, the first one jumps instantly.
As far as I can tell, React thinks that it can reuse one of the child elements, but not the other, although the docs say that if we use the key attribute, it should always reuse elements: https://facebook.github.io/react/docs/reconciliation.html (at least, that's how I understand it).
What should I change in my code to make it work as expected? Or is it a bug in React?
Live example: http://codepen.io/pavelp/pen/jAkoAG

caveat: I'm making some assumptions in this answer, nevertheless it shines light some of your (and previously my) questions. Also my solution can almost certainly be simplified, but for the purposes of answering this question it should be adequate.
This is a great question. I was a bit surprised to open up the dev tools and see what's actually happening when swapping the items.
If you take a look, you can sort of see what React is up to. The second element is not changing its style prop at all and just swaps the inner text node, while the first element is dropped into the dom as a fresh element.
If I had to guess, this is because of the way swapping two items in a array works, where at least one item is copied to a temp variable and placed back into the array.
I thought that maybe if you make the translation random, both elements would get new style props and animate, but that only made it more clear this was not the intended behaviour.
On the way to finding a solution:
As an experiment, what if we created the nodes ahead of time, and pass the index prop in render via React.cloneElement. While we're at it, let's render a span if index === 0 and a div otherwise. No keys to worry about.
http://codepen.io/alex-wilmer/pen/pbaXzQ?editors=1010
Opening up the dev tools now illustrates exactly what React is intending. React is preserving the elements and only changing the relevant part, in this case the innerText node and the element type. Because the styles are swapped exactly 1 : 1, no style update is needed.
Solution:
You can generate your React elements ahead of time, keep those in an array, and as such there are no keys to shuffle around and figure out how to place back into the DOM. Then use a different array to keep track of the intended order. Possibly highly convoluted, but it works!
http://codepen.io/alex-wilmer/pen/kXZKoN?editors=1010
const Item = function (props) {
return (
<div
style={{position: 'absolute',
transform: `translateY(${props.index * 20}px)`,
transition: '0.5s linear transform'}}>
{props.children}
</div>
)
}
const App = React.createClass({
getInitialState () {
return {
items: [
{item: 'One', C: <Item>One</Item>},
{item: 'Two', C: <Item>Two</Item>}
],
order: ['One', 'Two']
};
},
swap () {
this.setState({
order: [this.state.order[1], this.state.order[0]]
});
},
render: function () {
return <div>
<button onClick={this.swap}>Swap</button>
<div style={{position: 'relative'}}>
{this.state.items.map(x =>
React.cloneElement(x.C, {
index: this.state.order.findIndex(z => z === x.item)
}))
}
</div>
</div>;
}
});

Related

Im trying to build a menu that has collapsible options, but the dom does not update even when state data updates

const [arrayOfQuestions, setArrayOfQuestions] = useState([
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
{
q: 'Are your products safe to use?',
a: 'Yes.',
hidden: true
},
])
const toggleItemOpenAndClose = (e) => {
let array = arrayOfQuestions
array[e.target.id].hidden = !array[e.target.id].hidden
setArrayOfQuestions(array)
}
return (
<div>
<Layout
bgImage={metaData.heroImage.childImageSharp.fluid}
header='Frequently Asked Questions'>
<div className='page-container'>
<div className="content-container">
{
arrayOfQuestions.map((question,i) => {
return (
<div id={i} key={`id${i}`} onClick={toggleItemOpenAndClose} className='block-container'>
<div id={i} className='white smallerheader'>
{question.q}
</div>
{
question.hidden ?
null :
<div id={i} className='white paragraph'>
<br/>
{question.a}
</div>
}
</div>
)
})
}
</div>
</div>
</Layout>
</div>
)
}
Im using Gatsby and react hooks.
Im trying to build a collapsible menu (an faq) sort of like a menu, so that when you click on one of the options, it expands and shows you the answer associated with the question. However, I'm having trouble making it work in real time. whenever i click on the option, my data updates, but the dom itself doesnt update when i click on the option. then, when i change something in the code, the app updates (hot reloader?) and then the dom updates. As far as i understand it, when i change state in real time, the dom should also update in real time, but I can't understand why its not in this case. Has anyone experienced something like this before?
You should not be updating state directly. This should be your toggle code
const toggleItemOpenAndClose = (e) => {
// This will create a new array and with all new objects to preserve immutability
let newArray = arrayOfQuestions.map(x => {
return {... x}
}
newArray[e.target.id].hidden = !newArray[e.target.id].hidden
setArrayOfQuestions(newArray)
}
Make a copy arrayOfQuestions like so,
let array = [...arrayOfQuestions]
Why ?
What you're updating is an object property but it's still belongs to the same array ref. When you spread over the array, you will unpack the object references in your array and pack them in a new array due to those [] and then setting this new array will actually trigger the render.
In case you want to make copy of the objects as well in your array, use map as suggested by #Doug Hill which I think looks better.
But in your case simply spreading will also work for now. Things get messy when there are nesting depths of objects and then consider using libraries which supply you with deep cloning utilities.

Intersection Observer loses entries

I am trying to use IntersectionObserver in my react app. I want to achieve lazy loading with it. Therefore I observe several elements and if they appear on screen I load content inside them.
Here is a very simplified code:
class Table extends React.Component {
constructor() {
super()
this.state = {
entries: 0
}
}
componentWillUnmount() {
console.log('componentWillUnmount')
if (this.observer) this.observer.disconnect()
}
observe (c) {
if (!this.observer) {
this.observer = new IntersectionObserver(
entries => {
this.setState({entries: entries.length})
},
{ threshold: [0, 0.25, 0.5, 0.75, 1] }
)
}
if (!c) return
this.observer.observe(c)
}
render() {
const {entries} = this.state
return (
<div>
<h1>Number of observer entries: {entries}</h1>
<div
ref={this.observe.bind(this)}
style={{height: '1000px', display: 'block', width: '500px'}}
/>
<div
ref={this.observe.bind(this)}
style={{height: '1000px', display: 'block', width: '500px'}}
/>
</div>
)
}
}
ReactDOM.render(<Table />, document.querySelector("#app"))
When a component is mounted it shows two elements are observed but as soon as I scroll down it changes to only one element. I have not idea what I am missing.
JSON fiddle - https://jsfiddle.net/w1zn49q6/12/
The divs are stacked one after the other vertically. In the initial render, as they are laid out, the intersection observer gets triggered for both, as they enter the viewport together (the first div enters, the second div exits). However, once they are rendered, they will enter/exit one at a time on a normal course of vertical scrolling, hence the entries will only ever contain one div, which intersected the x-axis most recently.
The intersection entry only reports a transition to/from 0 (not in view) from/to 1 (fully in view). So when one div has fully exited/entered the view, it will no longer be present in an entry update.
You can still get 2 entries however :). If you manage to scroll really fast! Try it using an accelerated mouse wheel. So basically, between two intersection calculations, if both the divs moved too far, both will raise the intersection event, but if they move slowly, the intersection will be gradual because they are stacked one by one.
If you would stack them in the same row, you will continuously get two entries, as both will be intersecting at the same moment with the x-axis.

error: Do not use Array index in keys

I am using index to generate key in a list. However, es-lint generates an error for the same. React doc also states that using the item index as a key should be used as last resort.
const list = children.map((child, index) =>
<li key={index}> {child} </li>);
I considered using react-key-index.
npm install react-key-index gives following error:
npm ERR! code E404
npm ERR! 404 Not Found: react-key-index#latest
Are there any suggestions on other packages that allow to generate unique key? Any suggestion on react key generator is appreciated!
When you use index of an array as a key, React will optimize and not render as expected. What happens in such a scenario can be explained with an example.
Suppose the parent component gets an array of 10 items and renders 10 components based on the array. Suppose the 5th item is then removed from the array. On the next render the parent will receive an array of 9 items and so React will render 9 components. This will show up as the 10th component getting removed, instead of the 5th, because React has no way of differentiating between the items based on index.
Therefore always use a unique identifier as a key for components that are rendered from an array of items.
You can generate your own unique key by using any of the field of the child object that is unique as a key. Normal, any id field of the child object can be used if available.
Edit : You will only be able to see the behavior mentioned above happen if the components create and manage their own state, e.g. in uncontrolled textboxes, timers etc. E.g. React error when removing input component
The issue with using key={index} happens whenever the list is modified. React doesn't understand which item was added/removed/reordered since index is given on each render based on the order of the items in the array. Although, usually it's rendered fine, there are still situations when it fails.
Here is my example that I came across while building a list with input tags. One list is rendered based on index, another one based on id. The issue with the first list occurs every time you type anything in the input and then remove the item. On re-render React still shows as if that item is still there. This is đź’Ż UI issue that is hard to spot and debug.
class List extends React.Component {
constructor() {
super();
this.state = {
listForIndex: [{id: 1},{id: 2}],
listForId: [{id: 1},{id: 2}]
}
}
renderListByIndex = list => {
return list.map((item, index) => {
const { id } = item;
return (
<div key={index}>
<input defaultValue={`Item ${id}`} />
<button
style={{margin: '5px'}}
onClick={() => this.setState({ listForIndex: list.filter(i => i.id !== id) })}
>Remove</button>
</div>
)
})
}
renderListById = list => {
return list.map((item) => {
const { id } = item;
return (
<div key={id}>
<input defaultValue={`Item ${id}`} />
<button
style={{margin: '5px'}}
onClick={() => this.setState({ listForId: list.filter(i => i.id !== id) })}
>Remove</button>
</div>
)
})
}
render() {
const { listForIndex, listForId } = this.state;
return (
<div className='flex-col'>
<div>
<strong>key is index</strong>
{this.renderListByIndex(listForIndex)}
</div>
<div>
<strong>key is id</strong>
{this.renderListById(listForId)}
</div>
</div>
)
}
}
ReactDOM.render(
<List />,
document.getElementById('root')
);
.flex-col {
display: flex;
flex-direction: row;
}
.flex-col > div {
flex-basis: 50%;
margin: .5em;
padding: .5em;
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root">
<!-- This element's contents will be replaced with your component. -->
</div>
Of course, in React, you are required to pass in a unique key value for all elements of an array. Else, you will see this warning in console.
Warning: Each child in an array or iterator should have a unique “key” prop.
So, as a lazy developer, you would simply pass in the loop’s index value as the key value of the child element.
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
If your list is static and will not change.
The list will never be re-ordered.
The list will not be filtered (adding/removing item from
the list).
There are no ids for the items in the list.
Key should be unique but only among its siblings.
Do not use array indexes as keys, this is an anti-pattern that is pointed out by the React team in their docs.
It's a problem for performance and for state management. The first case applies when you would append something to the top of a list. Consider an example:
<ul>
<li>Element1</li>
<li>Element2</li>
<li>Element3</li>
</ul>
Now, let say you want to add new elements to the top/bottom of the list, then reorder or sort the list (or even worst - add something in the middle). All the index-based key strategy will collapse. The index will be different over a time, which is not the case if for each of these elements there would be a unique id.
CodePens:
Using index as a key: https://reactjs.org/redirect-to-codepen/reconciliation/index-used-as-key
Using ID as a key: https://reactjs.org/redirect-to-codepen/reconciliation/no-index-used-as-key
Play with it and you'll see that at some point the index-based key strategy is getting lost.
In map method ,react render our array element with respect to key. so key plays a vital role. if we use index as key then in this case , if we re-order our components then , we will get problem because react render our array element with respect to key.
For more details go through this video https://www.youtube.com/watch?v=Deu8GE3Xv50&list=PLBHcDEyoOGlQ4o_Nx6FCXmjYlOAF2am6V&index=4&t=363s
use the following lib "react-uuid" : https://www.npmjs.com/package/react-uuid.
react-uuid basically create random ids when you call it each time.
import React from 'react'
import uuid from 'react-uuid'
const array = ['one', 'two', 'three']
export const LineItem = item => <li key={uuid()}>{item}</li>
export const List = () => array.map(item => <LineItem item={item} />)
and this should solve the issue.

React - serious performance problems when changing one list item

I have come across a serious performance problem. I have an application with a list of 2000 quite a big DOM structure list items and when I am changing one item in the list component like this:
changeOne:function(){
var permTodos = this.props.todos;
permTodos[Math.floor((Math.random() * 5) + 0)].todo=((Math.random() * 50) + 1);
this.setState({ todos: permTodos });
},
it rerenders all of the list items in the virtual DOM representation, and this is really slow. The same thing is done in Angular1 about 40 times faster. I have tried to implement shouldComponentUpdate for the list items
shouldComponentUpdate:function(nextProps, nextState){
return this.props.todo!==nextProps.todo;
},
but the this.props.todo is the same as nextProps.todo so nothing is changed. I have recreated a mini-version of my application in this fiddle https://jsfiddle.net/2Lk1hr6v/29/ and I am hoping there are som React experts out there who could help me with this unfortunate situation.
You need to add unique key for each item from list:
this.state.todos.map(function(todo) {
return <TodoItem key={todo.id} todo={todo} done={this.done} />
}.bind(this))
Key must be associated with item for example it may be its id. It must be not its index in list where are all items lies.
Same edit here too, as mentioned in another thread.
Also I noticed two more things that I noticed in the fiddle here. In the TodoItem class, you have to say this.props.done not this.done.
render: function() {
return <li onClick={this.props.done}>{this.props.todo}</li>
}
In the TodoList class, you are referring to this.done which is not declared. You should probably declare that function?.
{
this.state.todos.map(function(todo) {
return <TodoItem todo={todo} done={this.done} />
}.bind(this))
}
Also switch to ES6 arrow functions! You don't have to bind this at the end of it. Code is much smaller and cleaner!!
{
this.state.todos.map(todo => <TodoItem todo={todo} done={this.done} />)
}
Check out the edited fiddle: https://jsfiddle.net/2Lk1hr6v/33/
This should be now much faster than what it was!
PS: Explore other ES6 features here!!

Where can I find the API guide for this usage of passing props to children in React

All:
Im pretty new to React-motion, when I read its source(Motion.js) there is one place like:
render: function render() {
var renderedChildren = this.props.children(this.state.currentStyle);
return renderedChildren && _react2['default'].Children.only(renderedChildren);
}
I get confused about this line:
var renderedChildren = this.props.children(this.state.currentStyle);
I wonder what does this line do, does it just give those style to children?
if so, where can I find the document for this usage in React website?
I also find one solution for passing props:
How to pass props to {this.props.children}
R they doing the same thing in diff ways?
Thanks
It is because in this particular case, the only child that is passed is a function. You can see that, for example, TransitionMotion component accepts a single child which is a function, for example:
<TransitionMotion willLeave={this.willLeave} styles={styles}>
{circles =>
<div
onMouseMove={this.handleMouseMove}
onTouchMove={this.handleTouchMove}
className="demo7">
{circles.map(({key, style: {opacity, scale, x, y}}) =>
<div
key={key}
className="demo7-ball"
style={{
opacity: opacity,
scale: scale,
transform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`,
WebkitTransform: `translate3d(${x}px, ${y}px, 0) scale(${scale})`,
}} />
)}
</div>
}
</TransitionMotion>
and in its propTypes, children has to be a function.
But in official docs (and in a usual way), children are instances of React.createElement, represented by DOM-like syntax in JSX, and being reflected from a plain-object, because React.createElement returns a plain-object.
The case with react-motion is very rare and advanced, although you may want to use this approach at some point if you find it more suitable than simply passing children components in a classic way.

Resources