Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid 'children with same key' when replacing state in ReactJs component

Tags:

reactjs

I have created a React Component that renders a set of sub-elements given an array of ids. The array of ids is kept in the state of the parent component, and then I run some ajax calls based on the ids to fetch data to render. The fetched data is stored in a separate data array in the state. The rendered components use the id as key.

The ids can change based on actions outside of the component, so I use setState on the component to replace the array. The updated id-state will probably contain some of the same ids as the in the original array. At the same time I empty the 'data array' so that everything will be rendered again.

When I do this I sometimes get the key-warning:

Warning: flattenChildren(...): Encountered two children with the same key. Child keys must be unique; when two children share a key, only the first child will be used.

The new array does not contain any duplicates. So why does it happen, and what can I do to avoid this?

Edit: Added some code by request. Note: I am using the Infinite Scroll module. Could this be causing it?

Initial state:

getInitialState: function() {   return {     hasMore: true,     num: 0,     movieIds: this.props.movieIds,     movies: []   }; }, 

Render function:

render: function() {   var InfiniteScroll = React.addons.InfiniteScroll;    return (     <InfiniteScroll         pageStart={0}         loadMore={this.loadMore}         threshold='20'         hasMore={this.state.hasMore}>         <ul className="movieList">           {this.state.movies}         </ul>     </InfiniteScroll>        ); } 

Simplified load more:

comp = this; $.ajax( {   url: url,   contentType: "json",   success: function (data) {     var m = createMovieLi(data);     var updatedMovies = comp.state.movies;     updatedMovies[num] = m;     comp.setState({movies: updatedMovies});   } }); 

And finally when updating outside the component:

movieBox.setState({   hasMore: true,   num: 0,   movieIds: filteredIds,   movies: [] }); 
like image 946
Øyvind Holmstad Avatar asked Sep 28 '14 19:09

Øyvind Holmstad


People also ask

Does React update all children components once the state of a parent has changed?

As we already saw before, React re-renders a component when you call the setState function to change the state (or the provided function from the useState hook in function components). As a result, the child components only update when the parent component's state changes with one of those functions.

Does changing the state of a parent component trigger a render on all of its child components in React?

Each state change in the parent component triggers re-rendering of all its subsequent child components even if no props are changed.

How do I change the state of my child's React component?

To change child component's state from parent component with React, we can pass props. const Child = ({ open }) => { return <Drawer open={open} />; }; const ParentComponent = () => { const [isOpen, setIsOpen] = useState(false); const toggleChildMenu = () => { setIsOpen((prevValue) => !

How do you change the state of another component React?

Sending state/props to another component using the onClick event: So first we store the state/props into the parent component i.e in which component where we trigger the onClick event. Then to pass the state into another component, we simply pass it as a prop.


1 Answers

I figured out my mistake, and it had nothing to do with React per se. It was a classic case of missing javascript closure inside a loop.

Because of the possibility of duplicates I stored each ajax response in window.localStorage, on the movieId. Or so I thought.

In React Inifinite Scroll each item in your list is drawn sequentially with a call to the loadMore-function. Inside this function I did my ajax call, and stored the result in the browser cache. The code looked something like this:

  var cachedValue = window.localStorage.getItem(String(movieId));   var cachedData = cachedValue ? JSON.parse(cachedValue) : cachedValue;    if (cachedData != null) {     comp.drawNextMovie(cachedData);   } else {      $.ajax( {       type: "GET",       url: this.state.movieUrl + movieId,       contentType: "json",       success: function (movieData) {         window.localStorage.setItem(String(movieId), JSON.stringify(movieData));         comp.drawNextMovie(movieData);       }     });     }     

Can you spot the mistake? When the ajax-call returns, movieId is no longer what is was. So I end up storing the data by the wrong id, and get some strange React warnings in return. Because this was done inside the loadMore function called by the InfiniteScroll-module, I was not aware that this function was not properly scoped.

I fixed it by adding a Immediately-invoked function expression.

like image 122
Øyvind Holmstad Avatar answered Sep 20 '22 10:09

Øyvind Holmstad