59

I'm trying to output a list of comma separated links and this is my solution.

var Item = React.createComponent({
  render: function() {

    var tags = [],
        tag;

    for (var i = 0, l = item.tags.length; i < l; i++) {
      if (i === item.tags.length - 1) {
        tag = <span><Tag key={i} tag={item.tags[i]} /></span>;
      } else {
        tag = <span><Tag key={i} tag={item.tags[i]} /><span>, </span></span>;
      }
      tags.push(tag);
    }

    return (
      <tr>
        <td>
          {item.name}
        </td>
        <td>
          {tags}
        </td>
      </tr>
    );

  }
});

I was just wondering if there was a better, more clean way to accomplish this?

Thanks

Nick
  • 755
  • 1
  • 7
  • 11

10 Answers10

112

Simply

{tags.map((tag, i) => <span key={i}>
    {i > 0 && ", "}
    <Tag tag={tag} />
</span>)}


In React 16 it can be done even more simpler:

{tags.map((tag, i) => [
    i > 0 && ", ",
    <Tag key={i} tag={tag} />
])}
iimos
  • 4,767
  • 2
  • 33
  • 35
  • 1
    Sweet and Simple! – Mrchief Feb 17 '17 at 19:54
  • @imos Would you mind explaining this a bit? `{!!i && ", "}` – Macxim Jul 29 '17 at 16:59
  • 2
    @Macxim This line prints comma before each tag except first. Expression `!!i` just converts `i` to boolean. Also `!!i` can be replaced by `i > 0`. – iimos Jul 31 '17 at 06:51
  • @imos Thank you very much. – Macxim Jul 31 '17 at 13:33
  • 3
    One of the best value-per-character answers, I've ever seen. Should I propose migrating this to Code Golf? =) – Boris Burkov Dec 06 '17 at 03:10
  • Will this append a comma after 1st element as well? The reason I'm asking, for the 1st element, index will be `o` and `!!0` will return `false`. Also I think this should add an additional comma after the last element. – Abhijith Sasikumar Apr 12 '18 at 01:18
  • @Abhijith At each iteration it thinks "Should i add comma *before* this item?". At 1st iteration it thinks "should i add comma at the very begining?". If we erase `!!i &&` it renders `, 0, 1 ...`. – iimos Apr 27 '18 at 11:56
  • Oh my mistake.. I was reading this as: `{tags.map((tag, i) => {!!i && ", "} )} ` – Abhijith Sasikumar May 03 '18 at 00:26
  • what's the use of the square brackets in the second example? `=> [...]` – kenold Nov 12 '19 at 21:20
  • @kenold Square brackets is just an array. Function returns an array of two ellements: comma string and react element. Whole `tags.map(...)` returns list of such pairs. React unwraps resulting nested set of arrays into flat list like `", "", " ", " ` and then renders it – iimos Nov 13 '19 at 11:36
  • @imos - Got it! thanks - I just used this on a Gatsby project. – kenold Nov 13 '19 at 15:24
  • Not sweet and not simple. Actually, this is way more complicated that it should be IMHO. Understood, though. – fracz Apr 06 '20 at 15:38
  • If there was a way to give "gold" (Reddit-style) on SO, this answer would get it from me. Thank you. – aalaap Aug 02 '22 at 09:33
75

At Khan Academy we use a helper called intersperse for this:

/* intersperse: Return an array with the separator interspersed between
 * each element of the input array.
 *
 * > _([1,2,3]).intersperse(0)
 * [1,0,2,0,3]
 */
function intersperse(arr, sep) {
    if (arr.length === 0) {
        return [];
    }

    return arr.slice(1).reduce(function(xs, x, i) {
        return xs.concat([sep, x]);
    }, [arr[0]]);
}

which allows you to write code like:

var tags = item.tags.map(function(tag, i) {
    return <Tag key={i} tag={item.tags[i]} />;
};
tags = intersperse(tags, ", ");
Sophie Alpert
  • 139,698
  • 36
  • 220
  • 238
  • 1
    Any idea how you could pass an element (like a ``) as the second parameter? It does work, but I get a warning about not specifying a key... I tried by wrapping the `` in a function (and generate a random key) but that doesn't seem very correct or to work. – thomasjonas Sep 15 '16 at 21:13
  • 1
    I just realised this is the way I could do it; pass a function as the separator that accepts and idx and then use the index of the array reducer as a key. – thomasjonas Sep 15 '16 at 21:20
  • 1
    Something like in [this gist](https://gist.github.com/thomasjonas/f99f48e278fd2dfe82edb2c6f7d6c365) – thomasjonas Sep 15 '16 at 21:26
9

Or simply write the list items to an unordered list and use CSS.

var Item = React.createComponent({
  render: function() {

    var tags = this.props.item.tags.map(function(i, item) {
      return <li><Tag key={i} tag={item} /></li>
    });

    return (
      <tr>
        <td>
          {this.props.item.name}
        </td>
        <td>
          <ul className="list--tags">
            {tags}
          </ul>
        </td>
      </tr>
    );

  }
});

And the CSS:

.list--tags {
    padding-left: 0;
    text-transform: capitalize;
}

.list--tags > li {
    display: inline;
}

.list--tags > li:before {
    content:',\0000a0'; /* Non-breaking space */
}
.list--tags > li:first-child:before {
    content: normal;
}
i_like_robots
  • 2,760
  • 2
  • 19
  • 23
  • 1
    It's really not explicit this way – S.C. Jul 08 '15 at 10:57
  • I like this approach. I just used `.csv` class and make it work with any element: `.csv>*`, not just `ui>li` and it's perfectly understandable – Liero May 09 '16 at 12:23
  • I like this approach because it's semantic, and as far as I can tell works better for accessibility. – tenor528 May 12 '17 at 21:37
2
import React from 'react';
import { compact } from 'lodash';

// Whatever you want to separate your items with commas, space, border...
const Separator = () =>  { ... }; 

// Helpful component to wrap items that should be separated
const WithSeparators = ({ children, ...props }) => {

  // _.compact will remove falsey values: useful when doing conditional rendering
  const array = compact(React.Children.toArray(children));

  return array.map((childrenItem, i) => (
    <React.Fragment key={`${i}`}>
      {i > 0 && <Separator {...props} />}
      {childrenItem}
    </React.Fragment>
  ));
};

const MyPage = () => (
  <WithSeparators/>
    <div>First</div>
    {second && (<div>Maybe second</div>)}
    {third && (<div>Maybe third</div>)}
    <div>Fourth</div>
  </WithSeparators>
);
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
2

A function component that does the trick. Inspired by @imos's response. Works for React 16.

const Separate = ({ items, render, separator = ', ' }) =>
  items.map((item, index) =>
    [index > 0 && separator, render(item)]
  )

<Separate
  items={['Foo', 'Bar']}
  render={item => <Tag tag={item} />}
/>
Alberto Jacini
  • 299
  • 3
  • 5
1

Here's a solution that allows <span>s and <br>s and junk as the separator:

const createFragment = require('react-addons-create-fragment');

function joinElements(arr,sep=<br/>) {
    let frag = {};
    for(let i=0,add=false;;++i) {
        if(add) {
            frag[`sep-${i}`] = sep;
        }
        if(i >= arr.length) {
            break;
        }
        if(add = !!arr[i]) {
            frag[`el-${i}`] = arr[i];
        }
    }
    return createFragment(frag);
}

It filters out falsey array elements too. I used this for formatting addresses, where some address fields are not filled out.

It uses fragments to avoid the warnings about missing keys.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
0

The solution without extra tags

<p className="conceps inline list">
  {lesson.concepts.flatMap((concept, i) =>
    [concept, <span key={i} className="separator">&#8226;</span>]
  , ).slice(-1)}
</p>

generates something like

Function • Function type • Higher-order function • Partial application

Ivan Kleshnin
  • 1,667
  • 2
  • 21
  • 24
0

Simple one:

{items.map((item, index) => (
    <span key={item.id}>
      {item.id}
      {index < items.length - 1 && ', '}
    </span>
 ))}
Abhijith Sasikumar
  • 13,262
  • 4
  • 31
  • 45
0

The easiest way to do

const elementsArr = ["a", "b", "c"];
let elementsToRender = [] ;
elementsArr.forEach((element, index) => {
    let elementComponent = <TAG className="abc" key={element.id}>{element}</TAG>
    elementsToRender.push(elementComponent);
    if(index !== (elementsArr.length - 1)){
        elementsToRender.push(", ");
    }
});

render(){
    return (
        <div>{elementsToRender}</div>
    )
}
nima
  • 7,796
  • 12
  • 36
  • 53
pareshm
  • 4,874
  • 5
  • 35
  • 53
0

To add to the great answers above Ramda has intersperse.

To comma separate a bunch of items you could do:

const makeLinks = (x: Result[]) =>
  intersperse(<>,</>, map(makeLink, x))

Pretty succinct

Damian Green
  • 6,895
  • 2
  • 31
  • 43