15

Yesterday I added react-router-dom to my project and now when I leave and come back to my Sky element in my nav, it reloads the sky and I get

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

(the number 50 used above is just an example, it throws this error ~40 times each time all with different ids)

The problem seems to stem from here in my sky.js file:

componentWillMount() {
    this.props.dispatch(requestSkySetup());
    this.props.dispatch(requestAllElements());

    this.setState({loadedSky: true, loadedElements: true});
}

Since each time I'm going to another screen, this component is unmounting and then re-mounting when I come back.

When receiveSkySetup is finished, the render function in sky.js creates a bunch of divs called Sectors and each Sector creates a few divs called Slots.

Then inside of Slot.render I have:

return connectDropTarget(
            <div className={showOutline ? 'slot showOutline' : 'slot'} style={style} onClick={interactable ? this.handleClick : null}>
                {
                    elements
                        .map(e => (
                            <SkyElement
                                id={e.id}
                                key={`element-id-${e.id}`}
                                title={e.title}
                                size={150}
                                opacity={e.opacity}
                                glow={e.glow}
                                color={e.color}
                                sectorId={e.sectorId}
                                slotId={e.id}
                                dispatch={this.props.dispatch}
                                isDragging={false}
                                transformElement={false} />
                        ))
                }
            </div>
        );

The key element in the SkyElement call above is what's throwing the 40+ errors on each mounting.

Happy to provide more code if needed.

Any help would be hugely helpful. Thanks!

Edit: Console logging elements

Digging in a bit more, the items are doubling in my store.

So, on the 2nd render of the sky tab, the full list of element ids is ["0", "1", "2", "3", "4", "5", "6", "7", "17", "18", "19", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "77", "78", "0", "1", "2", "3", "4", "5", "6", "7", "17", "18", "19", "55", "56", "57", "58", "59", "60", "61", "62", "63", "64", "65", "66", "67", "77", "78"]

On the 3rd render, elements 0-78 (the ids that apply from the array above) will be added again to the array

In Slot.js

const mapStateToProps = ({elements}, ownProps) => {
    return {
        elements: getElementsBySlotId(elements, ownProps.id),
    };
};

elements here will be n times the number of loads that Sky has done.

In sky.js

const mapStateToProps = ({sky, elements}) => {
    return {
        sectors: getSky(sky).sectors,
        elements: getElementsByKeyName(elements, 'visibleElements'),
        unplacedElements: getElementsByKeyName(elements, 'unplacedElements'),
    };
};

Printing elements.length I see that they double here too. Slot.js is pulling from the same store, so that makes sense

In my elements/reducer.js

case 'receiveAllElements':
        const visibleElements = {};
        const unplacedElements = {};

        const elements = action.elements.reduce((result, index) => {
            result[`${index.id}`] = index;
            return result;
        }, {});

        const keys = Object.keys(elements);
        for (const key of keys) {
            const e = elements[key];

            if (e.sectorId === null) {
                unplacedElements[key] = e;
            } else {
                visibleElements[key] = e;
            }
        }

        const visibleIds = Object.keys(visibleElements);
        const unplacedIds = Object.keys(unplacedElements);
        console.log(visibleIds);
        console.log(unplacedIds); // logging these, the numbers are consistent and don't double, triple etc with each load

        return {
            ...state,
            elementsMap: {
                ...state.elementsMap,
                ...elements,
            },
            visibleElements: [...state.visibleElements, ...visibleIds],
            unplacedElements: [...state.unplacedElements, ...unplacedIds],
        };

Maybe something in there is causing the count to double?

Zack Shapiro
  • 6,648
  • 17
  • 83
  • 151
  • Have you tried `.map((e, index) => (` and `key={index}` ? – Andrii Starusiev Jul 25 '17 at 21:56
  • I haven't but I'm not sure it would make a difference. For example, when I unmount and remount the Sky element, the error is thrown again for element-id-50 and if i repeat it, the error will happen again for 50. I imagine the same would happen with index – Zack Shapiro Jul 25 '17 at 22:06
  • Is each slot rendering the same `elements`? – Anthony Jul 25 '17 at 23:54
  • Each slot renders a uniq element or set of elements. There is no overlap – Zack Shapiro Jul 26 '17 at 02:52
  • can you update on how is the elements array populated? – duwalanise Jul 26 '17 at 03:47
  • It's pulled from the redux store which is filled as the result of the call to the server to get all unique elements for a user – Zack Shapiro Jul 26 '17 at 04:19
  • how about ``key={`${e.id}-${e.sectorId}`}``? – yadhu Jul 26 '17 at 06:32
  • I believe the issue here is that something is going on maybe with redux, not sure, where when the component is re-mounted, there's some existing record of the key somewhere. The ids are all unique, so adding the sectorId won't help – Zack Shapiro Jul 26 '17 at 15:00
  • @Andrew I tried your approach. Each time I come back to the Sky view from the other views, it adds an other element in each slot that wasn't there before (which obviously isn't ideal) – Zack Shapiro Jul 26 '17 at 15:24
  • @free-soul tried your approach as well, same bug as in the original post – Zack Shapiro Jul 26 '17 at 15:28
  • When you remove `connectDropTarget`, do the warnings disappear? – Koen. Jul 27 '17 at 21:55
  • Nope, they're still there after removing `connectDropTarget` – Zack Shapiro Jul 27 '17 at 21:59
  • @ZackShapiro take a look at the `elements` array maybe the are items with equal `e.id`. – Jay Jul 27 '17 at 22:09
  • All elements in the array have a unique id. The server-side validation wouldn't allow a new element to be created with an existing id – Zack Shapiro Jul 27 '17 at 22:14
  • @ZackShapiro Do you use react-css-modules? – Dawid Karabin Jul 28 '17 at 14:25
  • @hinok No, I do not – Zack Shapiro Jul 28 '17 at 15:02
  • @ZackShapiro does `this.props.dispatch(requestSkySetup());` populate your store with the sky elements by pushing them to an array? If so, I bet your store is not being cleaned when your component unmounts. So when it mounts again you have duplicate data in your store. – Kyle Richardson Jul 29 '17 at 03:43
  • Can you simply show use the console.log(elements) of when the warning happens? as the only reason there is same id... – Tzook Bar Noy Jul 29 '17 at 14:49
  • @TzookBarNoy Updating the original post with more info – Zack Shapiro Jul 29 '17 at 21:48
  • `result[\`${index.id}\`]` → `result[index.id]` – rishat Jul 29 '17 at 22:21
  • ooof, old code. Gross. Fixed that. Not the source of the problem though – Zack Shapiro Jul 29 '17 at 22:22
  • You could also try `action.elements.reduce((result, current, index) => ({ ...result, [index]: current }), {})`, but I'm not sure it'd make any sense. – rishat Jul 29 '17 at 22:23
  • Is the issue in the `return`ed object? is it doubling the values in there? – Zack Shapiro Jul 29 '17 at 22:26
  • Generally, object spread operation is super safe against duplicates. The language itself makes sure there are no duplicate keys in an object, it's just impossible. However, it's not true for arrays. This operation may produce duplicate _values_ in the array: `[...state.visibleElements, ...visibleIds]`. Given that you do `getElementsByKeyName(elements, 'visibleElements')`, that might be the cause. – rishat Jul 29 '17 at 22:26
  • Looks like I was able to fix it by changing the array to just `[...visibleIds]`. Is there a better solution than that or is that the one? Thanks for your help @RishatMuhametshin – Zack Shapiro Jul 29 '17 at 22:31
  • Do you want to leave an answer so I can give you the bounty? Thanks – Zack Shapiro Jul 29 '17 at 22:37
  • No problem and no need in this bounty thing! But regarding a better way to keep unique values in arrays, there's a good answer on that: https://stackoverflow.com/a/33121880/1287643 – rishat Jul 29 '17 at 23:32
  • @RishatMuhametshin, Please, answer on your question to people know your problem has a solution. – Slowyn Aug 02 '17 at 11:32

1 Answers1

8

The issue here was

    return {
        ...state,
        elementsMap: {
            ...state.elementsMap,
            ...elements,
        },
        visibleElements: [...state.visibleElements, ...visibleIds],
        unplacedElements: [...state.unplacedElements, ...unplacedIds],
    };

namely, the visibleElements (and unplacedElements values).

[...state.visibleElements, ...visibleIds] will concat 2 arrays so since this code was being hit each time I went back to the Sky tab, it was adding the new ids in ...visibleIds, to the array I already had in ...state.visibleElements and doubling values

Zack Shapiro
  • 6,648
  • 17
  • 83
  • 151