Search code examples
reactjschildrenreact-propsreact-component

Why does 'key' not propagate to react elements in map of {children} elements?


I'm implementing a component that makes it easier for users to add several items to a page. Since I have several types of items that may be added by a user, I figured having a single component with a "children" parameter would be most useful.

However, the implementation is having trouble with the "key" parameter when mapping over the children elements.

function ExpandableSet({
   startingSet,
   itemTitle,
   children
})
{

   const largestId = useMemo(() => {
      return startingSet.reduce((largestId, item) =>
         if (item.id > largestId){
            return item.id
         }
         return largestId
      }, 0)
   }, [])

   const [itemCount, setItemCount] = useState(largestId + 1);
   const [items, setItems] = useState(startingSet);

   return(
      <div className="expandable-set">
         { items.map( item =>
            <div className="expanded-item" id={item['key']} key={item['key']}> { children } </div>
         )}
      </div>
      <button onClick={() =>
         {
            items.push({"key" : itemCount});
            setItemCount(itemCount + 1);
         }
      }>
         Add {itemTitle}
      </button>
   )
}

When ExpandableSet is used, it's used like this:

...
<div className="container-for-expandable-set">
   <ExpandableSet
      startingSet={[]}
      itemTitle="Some item I want a lot of" />
      <SomeItem opts=opts />
   </ExpandableSet>
</div>
...

Since I have no idea how many SomeItem elements there will be, there's no way to pass keys at the parent level without managing state there as well. This would defeat the purpose of the ExpandableSet component (maybe this is the right answer?)

When this is output, I see the standard each child in a list should have a unique 'key' prop error.

HTML Output

...
<div class="expandable-set">
 <div class="expanded-item" id="0">
  <div class="child-template-passed">
   ...
  </div>
 </div>
 <div class="expanded-item" id="1">
  <div class="child-template-passed">
   ...
  </div>
 </div>
</div>
...

Why am I still seeing the key error?


Solution

  • You begin with a starting set which may have N elements, each with presumably any ID from zero upwards, and then a counter that starts at zero.

    So if I add an item to the set via the button on the page, it will have ID zero. But what if an item in the starting set already has an ID of zero? You now have a duplicate ID in the set.

    You need to find the largest ID in the starting set, and use this ID plus one as the initial value for itemCount.

    const largestID = useMemo(() => {
      return startingSet.reduce((largestID, item) => {
        if (item.key > largestID) {
          return item.key
        }
        return largestID
      }, 0)
    }, [startingSet])
    
    const [items, setItems] = useState(startingSet);
    const [itemCount, setItemCount] = useState(largestID + 1);