3

I am mapping through an array for which I want to assign a class to the first n elements. The first n elements are passed by the parent component and is steadily increasing.

I could use something like a ternary operator like className={index >= firstNElements ? '' : 'MyClass';}, however this would require all array items to be mapped through. In the case where theres several thousand items in the array and frequent prop changes, this seems rather ineffective. Is there a faster way to get this task done? Like constructing a while-loop for all elements whose index is smaller than firstNElements?

import React from "react";

export const myComponent = ({ firstNElements, myArray }) => {
   return(
      <div>
         {myArray.map((arrayItem) => (
           <span key={arrayItem.key}>{arrayItem.content}</span>
         ))}
      </div>
   );
}
Tobi
  • 363
  • 5
  • 15

3 Answers3

2

I could use something like a ternary operator like className={index >= firstNElements ? '' : 'MyClass';}

That's pretty much how you do it (but without the ;, the contents of that JSX expression are an expression, not a statement)

...however this would require all array items to be mapped through.

You do that any time the component is rendered, regardless of whether you're doing this as well. "Several thousand" isn't likely to be an issue in terms of the map or creating the spans (it's more likely a DOM rendering issue).

If your keys are consistent and the span's details haven't changed (it's the same class as last time, the same content as last time, etc.), React will leave the equivalent span that's in the DOM alone. It won't update or replace it when updating the DOM after a render if it hasn't changed.

Your component looks quite simple. Since it always creates the same output for the same props, you might use React.memo on it. From the documentation:

If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.

Note that "render" there means making the call to your component function to get the React elements for it, not DOM rendering.

Using memo on it would look like this:

export const myComponent = React.memo(({ firstNElements, myArray }) => {
   return(
      <div>
         {myArray.map((arrayItem) => (
           <span key={arrayItem.key}>{arrayItem.content}</span>
         ))}
      </div>
   );
});

If your component is more complex than shown and the spans are only a part of it, and you found a performance problem that you traced back to the map (rather than DOM rendering, etc.), you could memoize the set of spans using useMemo so you wouldn't need to use map unless something about the spans changed, but I wouldn't do that until/unless you've traced a specific problem to the map call itself.

FWIW, here's a naive comparison with and without the conditional expression:

// Why have warmup? The first time through triggers JIT, which makes the first
// run slower. So if I put with first, it's slower than without; if I put without
// first, it's slower than with. After the first time each has run, though,
// the JIT has *probably* done its work and we're looking at what you'll
// get from that point forward.
const MyComponentWith = ({ firstNElements, myArray, warmup = false }) => {
   if (!warmup) {
      console.time("with");
   }
   const spans = myArray.map((arrayItem, index) => (
           <span className={index >= firstNElements ? "" : "the-class"} key={arrayItem.key}>{arrayItem.content}</span>
         ))
   if (!warmup) {
      console.timeEnd("with");
   }
   return(
      <div>
         {spans}
      </div>
   );
};
const MyComponentWithout = ({ firstNElements, myArray, warmup = false }) => {
   if (!warmup) {
      console.time("without");
   }
   const spans = myArray.map((arrayItem) => (
           <span key={arrayItem.key}>{arrayItem.content}</span>
         ))
   if (!warmup) {
      console.timeEnd("without");
   }
   return(
      <div>
         {spans}
      </div>
   );
};

const items = Array.from(Array(30000), (_, i) => ({
   key: i,
   content: `Span #${i} `
}));

const first = 200;
ReactDOM.render(
   <div>
      <MyComponentWithout warmup={true} firstNElements={first} myArray={items} />
      <MyComponentWith warmup={true} firstNElements={first} myArray={items} />
      <MyComponentWithout firstNElements={first} myArray={items} />
      <MyComponentWith firstNElements={first} myArray={items} />
      <MyComponentWithout firstNElements={first} myArray={items} />
      <MyComponentWith firstNElements={first} myArray={items} />
      <MyComponentWithout firstNElements={first} myArray={items} />
      <MyComponentWith firstNElements={first} myArray={items} />
   </div>,
   document.getElementById("root")
);
.the-class {
    color: green;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

The results I get (Brave; essentially Chrome):

without: 4.940ms
with: 7.470ms
without: 3.480ms
with: 5.460ms
without: 9.180ms
with: 11.010ms

The conditional expression seems to cost about 2-3ms (in my setup, with that fairly naive test).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • The ternary operator option took about 15 ms in this case. I have 100ms inbetween rerenders. So this approach takes too long. Ill try the memo approach you suggested – Tobi Aug 21 '20 at 09:00
  • @Tobi - Are you sure you're measuring the cost of the conditional, though, and not the full map? Are you measuring just the first run, or subsequent runs? (The first will usually be slowest.) I've added a quick-and-dirty, fairly naive check above that suggests with 50k items, the conditional costs 2-3ms. Naturally, though, you want to avoid even that if the component produces the same result for the same inputs (so it's a good candidate for `React.memo`). – T.J. Crowder Aug 21 '20 at 09:21
  • The problem might actually be due to reducing the width to zero on the first n elements in "MyClass". This could be the performance problem Im getting. All following elements get shifted, but this is the desired effect. – Tobi Aug 21 '20 at 09:30
  • @Tobi - Ah, you're talking DOM rendering time. That's pretty much going to be the same regardless of how you React component's `map` works. They're entirely separate things. – T.J. Crowder Aug 21 '20 at 09:32
  • So theres really no way of optimizing here? – Tobi Aug 21 '20 at 09:33
  • Although Google Dev Tools tells me scripting is the big chunk of time. – Tobi Aug 21 '20 at 09:35
  • @Tobi - I listed two ways to optimize above. Whether you can use them, and whether they help in your situation, I don't know. You haven't mentioned trying either of them. But DOM rendering several thousand `span`s where a class makes some of them zero width is going to take time, no question. That part is out of scope for the question you asked, which was about creating the React stuff that gets used to create/update the DOM. – T.J. Crowder Aug 21 '20 at 09:38
0

Slice the array

You can chop it in half and do what you need to the first half. Also using slice will not mutate the orignal array value. mutation can be pretty bad, especially in react.I made an answer about why here

import React from "react";

export const myComponent = ({ firstNElements, myArray }) => {
   let arrayFirstPart = yourArray.slice(0, firstNElements);
   let arraySecondPart = yourArray.slice(firstNElements, myArray.length);
   return(
      <div>
         {arrayFirstPart.map((arrayItem) => (
           <span key={arrayItem.key} className="fist-set-of-values">{arrayItem.content}</span>
         ))}
         {arraySecondPart.map((arrayItem) => (
           <span key={arrayItem.key}>{arrayItem.content}</span>
         ))}
      </div>
   );
}
Joe Lloyd
  • 19,471
  • 7
  • 53
  • 81
  • I think it's really unlikely that the overhead of the `slice` calls and creation of the arrays they build is less than the overhead of repeating a conditional expression. Before complicating the code in this way, if it were my project I'd need hard evidence that this was better across our target browsers. (Not least because one's intuition -- anyone's, including mine! -- about these things can easily turn out to be wrong.) – T.J. Crowder Aug 21 '20 at 08:45
  • 1
    @T.J.Crowder completely agree, I would just map them and do an if on each iteration, like your answer suggests, +1. But I was expecting that he really has something quite heavy, that maybe he would like to optimise. But yes, these are assumptions. On a side note, I really admire your work on SO, your answers have helped me out on many occasions. – Joe Lloyd Aug 21 '20 at 08:50
0

As you clearly mentioned in your question, the way to go is to use

className={index >= firstNElements ? '' : 'MyClass'}

However because you are already mapping through the items, its just left for you to pass a second argument to the map function which will be the index and then you can compare it with the firstNElements basically something like below

import React from "react";

export const myComponent = ({ firstNElements, myArray }) => {
   return(
      <div>
         {myArray.map((arrayItem, index) => (
           <span key={arrayItem.key} className={index >= firstNElements ? '' : 'MyClass'}>{arrayItem.content}</span>
         ))}
      </div>
   );
}

React will decide which dom element to remount based on the change of content in the props which is what react is known to do.

harisu
  • 1,376
  • 8
  • 17