user7597670
user7597670

Reputation:

jsx map function check if object property is the same as the previous object's

I have an array of objects that looks like this:

const artists = [{
    date: '1/1/2017',
    name: 'Oasis'
  },
  {
    date: '5/1/2017',
    name: 'Blur'
  },
  {
    date: '5/1/2017',
    name: 'Simply Red'
  },
  {
    date: '10/1/2017',
    name: 'Ed Sheeran'
  },
  {
    date: '10/1/2017',
    name: 'Beyonce'
  },
  {
    date: '15/1/2017',
    name: 'Jay-z'
  },
];

I want to loop over the artists and if the date is different to the previous date I want to show that date above the artist name. The first artist should also show the date.

My code looks like this, and should hopefully make my question clearer.

render(){

    {artists.map((artist, index) =>

      {(artists[index].date !== artists[index - 1].date) && (
         <p>{ artists[index].date }</p>
      )}

      <div className="artists" key={index}>
        <p>{ artist.name }</p>  
      </div>

    )}

}

Upvotes: 0

Views: 1063

Answers (4)

Jordan Running
Jordan Running

Reputation: 106047

The problem is that in the first iteration of the loop, index is 0, so index - 1 is -1, and obviously artists[-1] is undefined.

A solution is to check if the current iteration is the first, which in your existing code would look like this:

(index === 0 || artist.date !== artists[index - 1].date) && (
  <p>{ artist.date }</p>
)

Your code won't really work as written, however, because in some iterations of the loop you want to return one element (the artist name), and in others you want to return two (the name and date). You could make it work with map, but it won't be very clean.

It would be cleaner with reduce:

artists.reduce((elms, {name, date}, index) => {
  if (index === 0 || date !== artists[index - 1].date) {
    elms = [ ...elms, <h4 key={date}>{date}</h4> ];
  }
  return [ ...elms, <p key={`${date}-${name}`}>{name}</p> ];          
}, [])

This code starts with an empty array ([]) and in each iteration adds a date if it's the first iteration or the date is different from the previous, followed by the name.

However, I suggest instead pre-processing your data to be closer to the shape that you're ultimately going to render. For example:

const artists = [
  { date: '1/1/2017', name: 'Oasis' },
  { date: '5/1/2017', name: 'Blur' },
  { date: '5/1/2017', name: 'Simply Red' },
  { date: '10/1/2017', name: 'Ed Sheeran' },
  { date: '10/1/2017', name: 'Beyonce' },
  { date: '15/1/2017', name: 'Jay-Z' },
];
    
const artistsByDate = artists.reduce((obj, {name, date}) => {
  const dateArr = obj[date] || [];
  dateArr.push(name);
  return { ...obj, [date]: dateArr };
}, {});

console.log(artistsByDate);
.as-console-wrapper{min-height:100%}

This code creates an object whose properties are dates and whose values are the artists associated with those dates.

Putting the data in the right "shape" before rendering it tends to make your rendering logic a lot simpler, which also makes it easier to generate a more semantic DOM structure:

function groupArtistsByDate(artists) {
  return artists.reduce((obj, artist) => {
    const dateArr = obj[artist.date] || [];
    dateArr.push(artist);
    return { ...obj, [artist.date]: dateArr };
  }, {});
}

const ArtistsDateGroup = ({date, artists}) => (
  <li>
    <h4>{date}</h4>
    <ul>
      {artists.map(({name}) => <li key={name}>{name}</li>)}
    </ul>
  </li>
);

const Artists = ({artists}) => (
  <ul>
    {Object.entries(groupArtistsByDate(artists)).map(([date, dateArtists]) => (
      <ArtistsDateGroup key={date} date={date} artists={dateArtists}/>
    )}
  </ul>
);

const artists = [
  { date: '1/1/2017', name: 'Oasis' },
  { date: '5/1/2017', name: 'Blur' },
  { date: '5/1/2017', name: 'Simply Red' },
  { date: '10/1/2017', name: 'Ed Sheeran' },
  { date: '10/1/2017', name: 'Beyonce' },
  { date: '15/1/2017', name: 'Jay-Z' },
];

ReactDOM.render(<Artists artists={artists}/>, document.querySelector('div'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div></div>

Upvotes: 1

Mayank Shukla
Mayank Shukla

Reputation: 104399

Use this:

let uiItems = [], prev = '';

artists.forEach((item, i) => {
   if(item.date != prev){
      uiItems.push(<div key={i}> {item.date} </div>);
   }
   uiItems.push(<div key={item.name}> {item.name} </div>);
   prev = item.date;
})

return uiItems;

Check this example:

const artists = [{
    date: '1/1/2017',
    name: 'Oasis'
  },
  {
    date: '5/1/2017',
    name: 'Blur'
  },
  {
    date: '5/1/2017',
    name: 'Simply Red'
  },
  {
    date: '10/1/2017',
    name: 'Ed Sheeran'
  },
  {
    date: '10/1/2017',
    name: 'Beyonce'
  },
  {
    date: '15/1/2017',
    name: 'Jay-z'
  },
];

let prev = '';

artists.map((item,i) => {
   if(item.date != prev){
      console.log(item.date, item.name);
   }else{
      console.log(item.name);
   }
   prev = item.date;
})

Upvotes: 0

Chad
Chad

Reputation: 618

artists[index - 1].date

This is undefined at 0 index because there's nothing to reference.

render(){

    {artists.map((artist, index) =>

      {(index > 0 && artists[index].date !== artists[index - 1].date) && (
         <p>{ artists[index]date }</p>
      )}

      <div className="artists" key={index}>
        <p>{ artist.name }</p>  
      </div>

    )}

}

Upvotes: 0

Arnelle Balane
Arnelle Balane

Reputation: 5487

The issue is in this line of code:

artists[index].date !== artists[index - 1].date

The value of the index variable starts at 0, so artists[index - 1] will try to get the item at index -1 which is undefined, as your error says. You can fix this by adding an additional condition to check if you are at index 0, and only do the index - 1 if you are not at index 0.

index === 0 || artists[index].date !== artists[index - 1].date

Upvotes: 0

Related Questions