Rendering random elements from an array in React

Tags: , ,



I am making a small react app with the help of Potter-API through which users can search for specific characters or spells. After fetching data from the API I am rendering 6 random items(characters/spells) which when clicked lead to a detailed view of the item(characters/spells), I’ve also added a button called randomize which when clicked renders a new set of random elements.

The issue I am facing is with this ‘randomize’ button, on clicking it repeatedly what’s happening is instead of rendering only 6 elements it starts to render 7, 8,… and breaks at some point resulting in an error.

I’d like to know what’s causing this and the fix for this.

class RandomItems extends React.Component {

    // this.props.randomNums contain the number of random characters to display
    // and the max limit of the items (this.props.data.length) and this.props.subUrl contains
    // the detailed-view URL(characters or spells) this.props.data is an array of item objects(characters/spells) out of
    // which some characters(some = this.props.randomNums) are chosen and rendered by this component
    constructor(props) {
        super(props);
        this.state = {
            itemsList: [],
            loading: true
        }

        this.handleRandoms = this.handleRandoms.bind(this)
    }


    componentDidMount() {
        const items = this.getRandomItems()
        this.setState({itemsList: items, loading: false})
    }

    handleRandoms(){
        const items = this.getRandomItems()
        this.setState({itemsList: items})
    }

    getRandomItems() {
        function getRandomNumbers(num, limit) {
            let randoms = []
            for (let i = 0; i < num; i++) {
                randoms.push(Math.floor(Math.random() * (limit + 1)))
            }
            return randoms
        }

        const randoms = getRandomNumbers(this.props.randomNums, this.props.data.length)
        return randoms.map(value => this.props.data[value])
    }


    // Each of the returned character should be a Link to the detail view of that character
    // Using the same component for both the spells/characters page so since the object attributes
    // are different for both categories I'm using a prop accessKey that is a string(name/spell) for 
    // accessing the specific attribute based on the item type(character/spell) 
    render() {
        if (this.state.itemsList && !this.state.loading) {
            return (
                <div style={{marginTop: '6em'}}>
                    <h2>Have Some Random {(this.props.subUrl)}!</h2>
                    <br/>
                    {this.state.itemsList.map((item, index) => {
                        return (
                            <div className={'characterDesign'} key={item._id}>


                                <Link className={'highlight-link'}
                                      to={`/${this.props.subUrl}/${item._id}`}
                                >
                                    {(index + 1) + '. ' + item[this.props.accessKey]}
                                </Link>

                            </div>
                        )
                    })}
                    <button className={'fill'} onClick={this.handleRandoms}>Randomize!</button>
                </div>
            )
        } else {
            return (<h1>Loading...</h1>)
        }
    }
}

The required array of data objects are sent from the parent component

  1. After some clicks of the randomize After some clicks of the randomize
  2. After many clicks of the randomize button After many clicks of the randomize button

PS. I’ve looked at the array that renders these items and every time it contains exactly 6 elements(even when higher number of elements are being rendered)

Answer

Your getRandomItems function can return the same item more than once and so when react renders the items there can be more than one with the same _id (which is being used as the key so multiple items can have the same key).

When you’ve got multiple <div>s with the same key attribute, react gets confused. The whole point of the key is to be unique. If you have multiple with the same key, react only cleans up the last one (for any given key) when rendering again.

Here is a minimalist example of the underlying issue:

class RandomItems extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            itemsList: [],
            loading: true
        };
    }

    componentDidMount() {
        const items = [
          this.props.data[0],
          this.props.data[0],
          this.props.data[0]
        ];
        this.setState({
          itemsList: items
        });
    }
  
    onClickTest = () => {
      const items = [
        this.props.data[1],
        this.props.data[2]
      ];
      this.setState({
        itemsList: items
      });
    };

    render() {
        return (
          <div>
            {this.state.itemsList.map((item, index) => {
              return (
                <div key={item.id}>
                  {item.name}
                </div>
              )
            })}
            <button onClick={this.onClickTest}>Test</button>
          </div>
        )
    }
}

/////////////////////////

ReactDOM.render(
  <RandomItems randomNums={3} data={[
      {id: 0, name: 'Zeroth'},
      {id: 1, name: 'First'},
      {id: 2, name: 'Second'}
  ]}></RandomItems>,
  document.getElementById('root')
);
<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"></div>

Notice when you click Test that the last of the three “0 Zeroth” divs is removed (as it should be) but the other two are not (because react is not expecting multiple divs with the same key).

The best solution in your case is probably to fix your randomize function so it never returns the same item multiple times. Example:

getRandomItems = () => {
  let allItems = [...this.props.data];
  const randomCount = this.props.randomNums;
  
  const randomItems = [];
  for (let i = 0; i < randomCount; i++) {
    const randomIndex = Math.floor(Math.random() * allItems.length);
    const randomItem = allItems.splice(randomIndex, 1)[0];
    randomItems.push(randomItem);
  }
  
  return randomItems;
};

Alternately, you could change the key from item._id to index which also fixes the issue because the index will always be unique.



Source: stackoverflow