Tobi
Tobi

Reputation: 365

Adding a class to first n elements in React map function dynamically

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>
   );
}

Upvotes: 3

Views: 2091

Answers (3)

T.J. Crowder
T.J. Crowder

Reputation: 1074208

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).

Upvotes: 2

harisu
harisu

Reputation: 1416

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.

Upvotes: 0

Joe Lloyd
Joe Lloyd

Reputation: 22353

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>
   );
}

Upvotes: 0

Related Questions