Key management on repeating elements in React

Viewed 679

I need a component that, given some children, it will repeat this children over and over to fill the screen. Let's say it has a simple markdown like this:

<FillScreen items={[
    { id: 'one', content: 'foo' },
    { id: 'two', content: 'bar' }
]} />

Internally, this component will get the screen size, and will eventually fill the screen with these items. The resulting DOM will look like this:

<div class="fill-screen">
   <div>foo</div>
   <div>bar</div>
   <div>foo</div>
   <div>bar</div>
   <div>foo</div>
   <div>bar</div>
   <div>foo</div>
</div>

The problem I have is that when this element has calculated that it needs to render 7 elements, I can't directly use items[i].id as a key for each element because there will be repeated keys.

I'm not sure what's the best pattern for this problem, I've thought two possible solutions which I don't fully like:

Solution A

Append a loop index on every key, so the array in render contains the following JSX:

   <div key='one-0'>foo</div>
   <div key='two-0'>bar</div>
   <div key='one-1'>foo</div>
   <div key='two-1'>bar</div>
   <div key='one-2'>foo</div>
   <div key='two-2'>bar</div>
   <div key='one-3'>foo</div>

Edit - note that these elements are being generated into an array, that's why we need to specify a key

I see two issues with this approach:

  • At this point I may just get rid of the original key and just use an index, something that's absolutely discouraged.
  • My real-life component, the way you give items to this component is through children (I oversimplified in here to get the point across), so I would have to clone each element just to change the key.

Solution B

Use fragments to separate between loops

   <Fragment key={0}>
     <div key='one'>foo</div>
     <div key='two'>bar</div>
   </Fragment>
   <Fragment key={1}>
     <div key='one'>foo</div>
     <div key='two'>bar</div>
   </Fragment>
   <Fragment key={2}>
     <div key='one'>foo</div>
     <div key='two'>bar</div>
   </Fragment>
   <Fragment key={3}>
     <div key='one'>foo</div>
   </Fragment>

In here it feels more clean, but at the same time this is not the reason Fragments were added in the first place, so maybe it's misusing them?.

Is there a better solution to this problem? What's the community standard?

2 Answers

Since you are using Fragment I assume that you are using version >16. In that case I think that there is another (perhaps better) way of solving this problem, which is returning an Array of Arrays. Something like this:

[
  [
    <div key='one'>foo</div>,
    <div key='two'>bar</div>,
  ],
  [
    <div key='one'>foo</div>,
    <div key='two'>bar</div>,
  ],
  [
    <div key='one'>foo</div>,
  ],
]

Actually this is "equivalent" to your "Solution B", but IMHO is less "hacky" because I think that Fragment is not meant to be used like that. Let me extend on that:

Before version 16 the render function couldn't return an Array of React Elements, which was something quite annoying. On version 16 that started being possible, however the elements of the Array must always have a key so that the React's reconciler works properly, meaning that returning this from a render function is bad:

[
  <span>foo</span>,
  <span>bar</span>,
] 

and this is correct:

[
  <span key="firstSpan">foo</span>,
  <span key="secondSpan">bar</span>,
] 

But doing that is a bit weird and annoying. That's why Fragment was invented, so that we can just return this:

<Fragment>
  <span>foo</span>
  <span>bar</span>
</Fragment>

So, the big advantage of Fragment is that we don't have to assign keys to their children elements. That's why I find it a bit hacky to see a Fragment with "keyed" children.

Also, I wanted to say that I don't think that there is anything wrong with your "Option A". However, I think that in most cases the solution that I'm proposing will be a lot easier to implement.

The right thing to use as key depends on what can change over the lifecycle of the parent component (FillScreen).

Using index as key is usually frowned upon because if items props changes during the lifecycle of FillScreen, then the same index can now point to another item but the component will not re-render because key remains the same.

In this case you can simply use a combination of id and index as the key but keep in mind that if the items get reshuffled in the array then the components for presenting every item will get remounted. The same is the case with the other approach using index keys on fragments (there is no particular advantage of using fragments here).

Similarly if you inject an item in between the array, then the components for presenting all subsequent items will get discarded and recreated.

If you do want to optimize for the above cases, you can construct the key by appending occurance index of that id.

<div class="fill-screen">
   <div key="one-0">foo</div>
   <div key="two-0">bar</div>
   <div key="one-1">foo</div>
   <div key="two-1">bar</div>
   <div key="one-2">foo</div>
   <div key="two-2">bar</div>
   <div key="one-3">foo</div>
</div>

React elements are fairly lightweight so cloning does not incur a significant cost overhead.

Related