reactgridlayout: Warning: Failed prop type: Duplicate child key "0" found

Viewed 116

What I am trying to do is have a new button to create only chart elements. However, opening console will display this error Warning: Failed prop type: Duplicate child key "0" found!

Below is my code

...

class MinMaxLayout extends React.PureComponent {
  static defaultProps = {
    items: 5,
    charts: 2,
    rowHeight: 30,
    cols: 12
  };

  constructor(props) {
    super(props);

    this.state = {
      items: [0, 1, 2, 3, 4].map(function (i, key, list) {
        return {
          i: i.toString(),
          x: i * 2,
          y: 0,
          w: 2,
          h: 2,
          add: i === list.length - 1
        };
      }),

      charts: [0, 1].map(function (i, key, list) {
        return {
          i: i.toString(),
          x: i * 2,
          y: 0,
          w: 2,
          h: 2,
          add: i === list.length - 1
        };
      }),
      newCounter: 0,
      chartCounter: 0
    };

    this.onAddItem = this.onAddItem.bind(this);
    this.onChartItem = this.onChartItem.bind(this);
    this.onBreakpointChange = this.onBreakpointChange.bind(this);
  }

  createElement(el) {
    const removeStyle = {
      position: "absolute",
      right: "2px",
      top: 0,
      cursor: "pointer"
    };
    const i = el.add ? "+" : el.i;
    return (
      <div key={i} data-grid={el}>
        {/* <span className="text">{i}</span> */}
        <span
          className="remove"
          style={removeStyle}
          onClick={this.onRemoveItem.bind(this, i)}
        >
          x
        </span>
      </div>
    );
  }

  createChart(el) {
    const removeStyle = {
      position: "absolute",
      right: "2px",
      top: 0,
      cursor: "pointer"
    };
    const i = el.add ? "+" : el.i;
    return (
      <div key={i} data-grid={el}>
        <span
          className="remove"
          style={removeStyle}
          onClick={this.onRemoveItem.bind(this, i)}
        >
          x
        </span>
      </div>
    );
  }

  onAddItem() {
    /*eslint no-console: 0*/
    console.log("adding", "n" + this.state.newCounter);
    this.setState({
      // Add a new item. It must have a unique key!
      items: this.state.items.concat({
        i: "n" + this.state.newCounter,
        x: (this.state.items.length * 2) % (this.state.cols || 12),
        y: Infinity, // puts it at the bottom
        w: 2,
        h: 2
      }),
      // Increment the counter to ensure key is always unique.
      newCounter: this.state.newCounter + 1
    });
  }

  onChartItem() {
    /*eslint no-console: 0*/
    console.log("adding chart", "n" + this.state.chartCounter);
    this.setState({
      // Add a new item. It must have a unique key!
      charts: this.state.charts.concat({
        i: "n" + this.state.chartCounter,
        x: (this.state.charts.length * 2) % (this.state.cols || 12),
        y: Infinity, // puts it at the bottom
        w: 8,
        h: 6
      }),
      // Increment the counter to ensure key is always unique.
      chartCounter: this.state.chartCounter + 1
    });
  }

  // We're using the cols coming back from this to calculate where to add new items.
  onBreakpointChange(breakpoint, cols) {
    this.setState({
      breakpoint: breakpoint,
      cols: cols
    });
  }

  onRemoveItem(i) {
    console.log("removing", i);
    this.setState({ items: _.reject(this.state.items, { i: i }) });
  }

  render() {
    return (
      <div className="DetailLocationContainer">
        <button onClick={this.onAddItem}>Add Item</button>
        <button onClick={this.onChartItem}>Add Chart</button>
        <ReactGridLayout
          onBreakpointChange={this.onBreakpointChange}
          {...this.props}
        >
          {_.map(this.state.items, (el) => this.createElement(el))}
          {_.map(this.state.charts, (el) => this.createChart(el))}
        </ReactGridLayout>
      </div>
    );
  }
}

...

Expected result: no error message

Actual result: Warning: Failed prop type: Duplicate child key "0" found

Click this link for my codesandbox. will appreciate any help

1 Answers

Issue

You are rendering two sets of data into a component, using array indices as keys, and so you've duplicate keys among the entire set of siblings.

Solution

You shouldn't use array indices as React keys as they don't generally provide uniqueness, as you've found out, and they aren't intrinsic to the array elements, generally.

I suggest adding a GUID property to each object you create, and use this id property as the React key.

import { v4 as uuidV4 } from 'uuid';

...

this.state = {
  items: [0, 1, 2, 3, 4].map(function (i, key, list) {
    return {
      id: uuidV4(), // <-- id property
      ....
    };
  }),
  charts: [0, 1].map(function (i, key, list) {
    return {
      id: uuidV4(), // <-- id property
      ....
    };
  }),
  ....
};

...

<div key={el.id} data-grid={el}> // <-- el.id is React key
  ...
</div>

Edit reactgridlayout-warning-failed-prop-type-duplicate-child-key-0-found

Full code:

import React from "react";
import ReactDOM from "react-dom";
import RGL, { WidthProvider } from "react-grid-layout";
import _ from "lodash";
import "./styles.css";
import { v4 as uuidV4 } from "uuid";

const ReactGridLayout = WidthProvider(RGL);

class MinMaxLayout extends React.PureComponent {
  static defaultProps = {
    items: 5,
    charts: 2,
    rowHeight: 30,
    cols: 12
  };

  constructor(props) {
    super(props);

    this.state = {
      items: [0, 1, 2, 3, 4].map(function (i, key, list) {
        return {
          id: uuidV4(),
          i: i.toString(),
          x: i * 2,
          y: 0,
          w: 2,
          h: 2,
          add: i === list.length - 1
        };
      }),

      charts: [0, 1].map(function (i, key, list) {
        return {
          id: uuidV4(),
          i: i.toString(),
          x: i * 2,
          y: 0,
          w: 2,
          h: 2,
          add: i === list.length - 1
        };
      }),
      newCounter: 0,
      chartCounter: 0
    };

    this.onAddItem = this.onAddItem.bind(this);
    this.onChartItem = this.onChartItem.bind(this);
    this.onBreakpointChange = this.onBreakpointChange.bind(this);
  }

  createElement(el) {
    const removeStyle = {
      position: "absolute",
      right: "2px",
      top: 0,
      cursor: "pointer"
    };
    const i = el.add ? "+" : el.i;
    return (
      <div key={el.id} data-grid={el}>
        {/* <span className="text">{i}</span> */}
        <span
          className="remove"
          style={removeStyle}
          onClick={this.onRemoveItem.bind(this, i)}
        >
          x
        </span>
      </div>
    );
  }

  createChart(el) {
    const removeStyle = {
      position: "absolute",
      right: "2px",
      top: 0,
      cursor: "pointer"
    };
    const i = el.add ? "+" : el.i;
    return (
      <div key={el.id} data-grid={el}>
        <span
          className="remove"
          style={removeStyle}
          onClick={this.onRemoveItem.bind(this, i)}
        >
          x
        </span>
      </div>
    );
  }

  onAddItem() {
    /*eslint no-console: 0*/
    console.log("adding", "n" + this.state.newCounter);
    this.setState({
      // Add a new item. It must have a unique key!
      items: this.state.items.concat({
        id: uuidV4(),
        i: "n" + this.state.newCounter,
        x: (this.state.items.length * 2) % (this.state.cols || 12),
        y: Infinity, // puts it at the bottom
        w: 2,
        h: 2
      }),
      // Increment the counter to ensure key is always unique.
      newCounter: this.state.newCounter + 1
    });
  }

  onChartItem() {
    /*eslint no-console: 0*/
    console.log("adding chart", "n" + this.state.chartCounter);
    this.setState({
      // Add a new item. It must have a unique key!
      charts: this.state.charts.concat({
        id: uuidV4(),
        i: "n" + this.state.chartCounter,
        x: (this.state.charts.length * 2) % (this.state.cols || 12),
        y: Infinity, // puts it at the bottom
        w: 8,
        h: 6
      }),
      // Increment the counter to ensure key is always unique.
      chartCounter: this.state.chartCounter + 1
    });
  }

  // We're using the cols coming back from this to calculate where to add new items.
  onBreakpointChange(breakpoint, cols) {
    this.setState({
      breakpoint: breakpoint,
      cols: cols
    });
  }

  onLayoutChange(layout) {
    this.props.onLayoutChange(layout);
    this.setState({ layout: layout });
  }

  onRemoveItem(i) {
    console.log("removing", i);
    this.setState({ items: _.reject(this.state.items, { i: i }) });
  }

  render() {
    return (
      <div className="DetailLocationContainer">
        <button onClick={this.onAddItem}>Add Item</button>
        <button onClick={this.onChartItem}>Add Chart</button>
        <ReactGridLayout
          onBreakpointChange={this.onBreakpointChange}
          {...this.props}
        >
          {_.map(this.state.items, (el) => this.createElement(el))}
          {_.map(this.state.charts, (el) => this.createChart(el))}
        </ReactGridLayout>
      </div>
    );
  }
}

module.exports = MinMaxLayout;

if (require.main === module) {
  require("../test-hook.jsx")(module.exports);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<MinMaxLayout />, rootElement);
Related