React messed up states in list [duplicate] - javascript

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.

Related

React gives wrong index to Child with React.memo

I've got a problem in my project which is pretty large therefore I can't post every part of the code here, but I'll try to summarize the main parts.
I've got a parent component which state is managed through a useReducer and which render returns a mapping of this state.
Each child has to take both the value and the index of the mapping. I'm also using the Context API to pass the dispatcher
on some of the childs.
function ParentComponent(props) {
const [state, dispatch] = useReducer(initialData, reducer);
return (
<div>
<MyContext.Provider value={dispatch}>
{state.myArray.map((value, index) => (
<ChildComponent value={value} index={index} />
))}
</MyContext.Provider>
</div>
);
}
/* Other file */
function ChildComponent({value, index}) {
const dispatch = useContext(MyContext);
return <div>
{/* Uses value and index to display some data */}
</div>
}
export default React.memo(ChildComponent, (prevProps, nextProps) => !_.isEqual(prevProps, nextProps));
Some of the childs in the tree components have to use React.memo to avoid useless re-renders, ChildComponent is one of them.
I'm using the lodash functions _.isEqual to compare the props to know when the component has to re-render.
The problem is the following. One of my components in the component tree adds items to the myArray attribute of the ParentComponent state.
When I add an item, anyway, each ChildComponent rendered from the ParentComponent mapping receives the same index of 0, creating a lot of problems.
The ParentComponent has the right indices (when I log them inside the mapping they are the right ones) but it's like the ChildComponent isn't getting it.
Is there any way to solve this? Maybe it has something to do with React.memo?
React.memo() takes a callback that should determine whether prevProps and nextProps are equal, but you're returning the negation of that.
Also using the index as a key should be considered a last resort, since this will fail to behave correctly when elements in the array are re-ordered relative to each other. You should always source the key from the data whenever possible.

React - adding key interferes with state of children

having a parent element which requires a key (because it's in a list creating a warning message), I added key={uuid.v4()}.
This made the message disappear.
Funny things started happening with it's child components, though. When I use the functional setState hook, it doesn't actually assign it to the value anymore (see below [1]). When I remove the key from the parent Component, the same code works (but leaves me with the warning).
When adding a static key, e.g. key={'someComponent'}, the whole component doesn't render at all.
Any tips what I'm missing here?
[1] Child Component which does not update it's state:
function zoomIntoRegion(countryName, filter) {
props.changeFilter({SLMetric: filter})
if (regionZoom === countryName && filter === props.filter) {
setRegionZoom(undefined)
} else {
console.log('before setzoom', countryName) // # before setzoom GERMANY
setRegionZoom(countryName)
console.log('after', regionZoom) // # after undefined
}
}
Keys in React are used for quickly comparing the current children to the children before any change occured. When you set the key on a component to uuid.v4() in the render function, every time the parent component rerenders, you can see that it generates a new key. From the docs:
Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.
which seems to accurately define what you are facing.
To work around this problem,
If you do not see the order of your children changing at all, then it is okay to use index as key.
If you do see it changing and have no key in the data that you can use, then move the key generation setup to where you are fetching/generating the data so that it is executed only once. Ensure that keys do not change on render, but only when the data itself is changing.
function App() {
const [items, setItems] = useState([]);
useEffect(() => {
getData()
// mapping over array data and adding the key here
.then((data) => data.map((item) => ({...item, id: uuid.v4() })))
.then((data) => setItems(data))
}, []);
return (
<Fragment>
// using the key
{items.map((item) => {
<div key={item.id}>
</div>
})}
</Fragment>
)
}

Reactjs - Keys on custom components in list

We have an object 'Schema' with inputs for a form. We map through the inputs and return a custom component for each that then does the rest of the work. The problem is that:
1) Custom components can't have a key attribute, as key is a special property used by React and is not passed to the children.
2) The first item in a list must have a key attribute so that React can know how to update the components.
If your first item in the list is a custom Component, then these two are mutually exclusive. We can get around this problem by surrounding the QuestionBlock with a div or any other generic element and adding a key attribute to it. However this results in code and HTML that looks sloppy, having excessive divs just to work around this problem. Is there any other cleaner way to do it. Below is the code we had before:
render() {
return (
<div className="App">
{Object.keys(Schema.inputs).map((key,index) => {
let data = Object.assign({}, Schema.inputs[key]);
data.index = index;
return <QuestionBlock key={index} {...data} />;
}}
</div>
);
}
Warning shown in console when using the above code:
Warning: QuestionBlock: key is not a prop. Trying to access it will
result in undefined being returned. If you need to access the same
value within the child component, you should pass it as a different
prop.
And below here is the code that works, but looks messy:
render() {
return (
<div className="App">
{Object.keys(Schema.inputs).map((key,index) => {
let data = Object.assign({}, Schema.inputs[key]);
data.index = index;
return <div key={index}>
<QuestionBlock {...data} />
</div>;
}}
</div>
);
}
Many thanks in advance!
Instead of wrapping using div, use a keyed Fragment.
Fragment is a container that doesn't generate HTML so it won't clutter your HTML output.
And also you can use a short-hand version <>, not <Fragment>

Nested component list does not update correctly

I have a recursively defined component tree which is something like this:
class MyListItem extends Component {
...
componentDidMount() {
this.listener = dataUpdateEvent.addListener(event, (newState) => {
if(newState.id == this.state.id) {
this.setState(newState)
}
})
}
...
render() {
return (
<div>
<h1>{this.state.title}</h1>
<div>
{this.state.children.map( child => {
return (<MyListItem key={child.id} data={child} />)
})}
</div>
</div>
)
}
}
So basically this view renders a series of nested lists to represent a tree-like data structure. dataUpdateEvent is triggered various ways, and is intended to trigger a reload of the relevant component, and all sub-lists.
However I'm running into some strange behavior. Specifically, if one MyListItem component and its child update in quick succession, I see the top level list change as expected, but the sub-list remains in an un-altered state.
Interestingly, if I use randomized keys for the list items, everything works perfectly:
...
return (<MyListItem key={uuid()} data={child} />)
...
Although there is some undesirable UI lag. My thought is, maybe there is something to do with key-based caching that causes this issue.
What am I doing wrong?
React uses the keys to map changes so you need those. There should be a warning in the console if you don't use unique keys. Do you have any duplicate ids? Also try passing all your data in as props instead of setting state, then you won't need a listener at all.

React is rerendering my list even though each child in array has its unique key

So, as far as I understand react only rerenders new elements with new keys. Thats not working for me though.
I have a list of posts, that are limited to 3.
When the user scrolls to bottom of page I add 3 to the limit, which means at the bottom of the page 3 older posts are supposed to be shown.
What I have now works, but the entire list is being rerendered. And it jumps to the top which is also not wanted (this I can fix though, main problem is the rerendering). They all have unique keys. How can I prevent this behaviour?
thisGetsCalledWhenANewPostComesIn(newPost){
let newPosts = _.clone(this.state.posts);
newPosts.push(newPost);
newPosts.sort((a,b) => b.time_posted - a.time_posted);
this.setState({posts: newPosts});
}
render(){
return (
<div ref={ref => {this.timelineRef = ref;}} style={styles.container}>
{this.state.posts.map(post =>
<Post key={post.id} post={post} />
)}
</div>
);
}
Having unique keys alone does not prevent rerendering components that have not changed. Unless you extend PureComponent or implement shouldComponentUpdate for the components, React will have to render() the component and compare it to the last result.
So why do we need keys when it's really about shouldComponentUpdate?
The purpose of giving each component in a list a unique key is to pass the props to the "right" component instances, so that they can correctly compare new and old props.
Imagine we have a list of items, e.g.:
A -> componentInstanceA
B -> componentInstanceB
C -> componentInstanceC
After applying a filter, the list must be rerendered to show the new list of components, e.g.:
C -> ?
Without proper unique keys, the component that previously rendered A will now receive the prop(s) for C. Even if C is unchanged, the component will have to rerender as it received completely different data:
C -> componentInstanceA // OH NO!
With proper unique keys, the component that rendered C will receive C again. shouldComponentUpdate will then be able to recogize that the render() output will be the same, and the component will not have to rerender:
C -> componentInstanceC
If your list of items take a long time to render, e.g. if it's a long list or each element is a complex set of data, then you will benefit from preventing unnecessary rerendering.
Personal anecdote
In a project with a list of 100s of items which each produced 1000s of DOM elements, changing from
list.map((item, index) => <SomeComp key={index} ... />)
to
list.map(item => <SomeComp key={item.id} ... />)
reduced the rendering time by several seconds. Never use array index as key.
You will have to implement shouldComponentUpdate(nextProps, nextState) in the Post component. Consider extending the PureComponent class for the Post component instead of the default React Component.
Good luck!
PS: you can use a string as ref parameter for your div in the render method like so:
render() {
return (
<div
ref='myRef'
style={styles.container}
>
{this.getPostViews()}
</div>
);
}
Then, if you want to refer to this element, use it like this.refs.myRef. Anyway, this is just a personal preference.
Okay, my bad. I thought I'd only post the "relevant" code, however it turns out, the problem was in the code I left out:
this.setState({posts: []}, ()=> {
this.postListenerRef = completedPostsRef.orderByChild('time')
.startAt(newProps.filter.fromDate.getTime())
.endAt(newProps.filter.toDate.getTime())
.limitToLast(this.props.filter.postCount)
.on('child_added', snap => {
Database.fetchPostFromKey(snap.key)
.then(post => {
let newPosts = _.clone(this.state.posts);
newPosts.push(_.assign(post, {id: snap.key}));
newPosts.sort((a,b) => b.time_posted - a.time_posted);
this.setState({posts: newPosts});
}).catch(err => {throw err;});
});
});
I call setState({posts: []}) which I am 99% sure is the problem.

Categories