DEV Community

Cover image for Embedding Data Into React/JSX Elements
Adam Nathaniel Davis
Adam Nathaniel Davis

Posted on • Updated on

 

Embedding Data Into React/JSX Elements

What I'm about to show you is really pretty basic. So Code Gurus out there can feel free to breeze on by this article. But I've rarely seen this technique used, even in "established" codebases crafted by senior devs. So I decided to write this up.

This technique is designed to extract bits of data that have been embedded into a JSX (or... plain ol' HTML) element. Why would you need to do this? Well... I'm glad you asked.


Image description

The Scenario

Let's look at a really basic function:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td key={`cell-${rowIndex}-${cellIndex}`}>
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

This function simply builds a particular row of table cells. It's used in my https://paintmap.studio app to build a "color map". It generates a giant grid (table) that shows me, for every block in the grid, which one of my paints most-closely matches that particular block.

Once this feature was built, I decided that I wanted to add an onClick event to each cell. The idea is that, when you click on any given cell, it then highlights every cell in the grid that contains the same color as the one you've just clicked upon.

Whenever you click on a cell, the onClick event handler needs to understand which color you've chosen. In other words, you need to pass the color into the onClick event handler. Over and over again, I see code that looks like this to accomplish that kinda functionality:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={paintIndex => handleCellClick(paintIndex)}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

The code above isn't "wrong". It will pass the paintIndex to the event handler. But it isn't really... optimal. The inefficiency that arises is that, for every single table cell, we're creating a brand new function definition. That's what paintIndex => handleCellClick(paintIndex) does. It spins up an entirely new function. If you have a large table, that's a lotta function definitions. And those functions need to be redefined not just on the component's initial render, but whenever this component is re-invoked.

Ideally, you'd have a static function definition for the event handler. That would look something like this:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={handleCellClick}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

In the above code, the handleCellClick has already been defined outside this function. So React doesn't need to rebuild a brand new function definition every single time that we render a table cell. Unfortunately, this doesn't entirely work either. Because now, every time the user clicks on a table cell, the event handler will have no idea which particular cell was clicked. So it won't know which paintIndex to highlight.

Again, the way I normally see this implemented, even in well-built codebases, is to use the paintIndex => handleCellClick(paintIndex) approach. But as I've already pointed out, this is inefficient.

So let's look at a couple ways to remedy this.


Image description

Wrapper Components

One approach is to create a wrapper component for my table cells. That would look like this:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <MyTableCell
            key={`cell-${rowIndex}-${cellIndex}`}
            paintIndex={paintIndex}
         >
            {paintIndex}
         </MyTableCell>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

In this scenario, we're no longer using the base HTML attribute of <td>. Instead, there's a custom component, that will accept paintIndex as a prop, and then presumably use that prop to build the event handler. MyTableCell would look something like this:

const MyTableCell = paintIndex => {
   const handleCellClick = () => {
      // event handler logic using paintIndex
   }

   return (
      <td onClick={handleCellClick}>
         {paintIndex}
      </td>
   )
}
Enter fullscreen mode Exit fullscreen mode

Using this approach, we don't have to pass the paintIndex value into the handleCellClick event handler, because we can simply reference it from the prop. There's much to like in this solution because it's consistent with an idiomatic approach to React. Ideally, you'd even memoize the MyTableCell component so it doesn't get remounted (and re rendered) every time we build a table cell that uses the same paint color.

However, this approach can also feel a bit onerous because we're cranking out another component purely for the sake of making that one onClick event more efficient. Also, if the handleCellClick event handler needs to do other logic that impacts that state in the calling component, the resulting code can get a bit "heavy".

Sometimes you want the logic for that event handler to be handled right inside the calling component. Luckily, there are other ways to do this.


Image description

HTML Attributes

HTML affords us a lot of freedom to "stuff" data where it's needed. For example, you could use the longdesc attribute to embed the paintIndex right into the HTML element itself. Unfortunately, longdesc is only "allowed" in <frame>, <iframe>, and <img> elements.

Granted, browsers are tremendously forgiving about the usage of HTML attributes. So if you were to start putting longdesc attributes on all sorts of "illegal" HTML elements, it really won't break anything. The browser will basically just ignore the non-idiomatic attributes. In fact, you can even add your own custom attributes to HTML elements.

Nevertheless, it's usually good practice to avoid stuffing a buncha non-allowed or completely-custom attributes into your HTML elements. But we have more options. More "standard" options.

(Near) Universal HTML Attributes

If you wanna find an attribute that you can put on pretty much any element, the first things is to look at the attributes that are allowed in (almost) any elements. They are as follows:

  • id
  • class
  • style
  • title
  • dir
  • lang / xml:lang

The nice thing about these attributes is that you can pretty much use them anywhere within the body of your HTML, on pretty much any HTML element, and you don't have to worry about whether they're "allowed". You can, for example, put a title attribute on a <div>, or a dir attribute on a <td>. It's all "acceptable" - by HTML standards, that is.

So if you wanted to use one of these attributes to "pass" data into an event handler, what would be the best choice?

title

First of all, as tempting as it may be to use something like title, I would not recommend this. title is used by screen readers and you're gonna jack up the accessibility of your site if you stuff a bunch of programmatic data into that attribute - data that should not be read by a screen reader.

dir, lang, xml:lang

Similarly, you should avoid appropriating the dir, lang, or xml:lang attributes. Messing with these attributes could jack up the utility of the site for international users (i.e., those who are using your site with a different language). So please, leave those alone as well.

style

Also, you could try to cram "custom" data into a style attribute. But IMHO, that's gonna come out looking convoluted. In theory, you could define a custom style property like this:

<td style={{paintIndex}}>
Enter fullscreen mode Exit fullscreen mode

Then you could try to read this made-up style property on the element when it's captured in the event handler. But... I don't recommend such an approach. Not at all. First, if you have any legitimate style properties set on the element, you're gonna end up with a mishmash of real and made-up properties. Second, there's no reason to embed data in such a verbose format. You can do it much "cleaner" with the other options at our disposal.

id

The id attribute can be a great place to "embed" data. Here's what that would look like:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            id={paintIndex}
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={handleCellClick}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

Here, we're using the paintIndex as the id. Why would we do this? Because then we can create an event handler that looks like this:

const handleCellClick = (event = {}) => {
   uiState.toggleHighlightedColor(event.target.id);
}
Enter fullscreen mode Exit fullscreen mode

This works because the synthetic event that's passed to the event handler will have the id of the clicked element embedded within it. This allows us to use a generic event handler on each table cell, while still allowing the event handler to understand exactly which paintIndex was clicked upon.

This can still have some drawbacks. First of all, ids are supposed to be unique. In the example above, a given paintIndex may be present in a single table cell - or in hundreds of them. And if we simply use the paintIndex value as the id, we'll end up with many table cells that have identical id values. (To be clear, having duplicate ids won't break your HTML display. But in some scenarios it can break your JavaScript logic.)

Thankfully, we can fix that, too. Notice that our table cells have key values. And keys must be unique. In this scenario, I addressed that problem by using the row/cell counts to build the key. Because no two cells will have the same combination of row/cell numbers. We can add the same format to our id. That looks like this:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            id={`cell-${rowIndex}-${cellIndex}-${paintIndex}`}
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={handleCellClick}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

Now, there will never be a duplicate id on any of our cells. But we still have the paintIndex value embedded into that id. So how do we extract the value in our event handler? That looks like this:

const handleCellClick = (event = {}) => {
   const paintIndex = event.target.id.split('-').pop();
   uiState.toggleHighlightedColor(paintIndex);
}
Enter fullscreen mode Exit fullscreen mode

Since we wrote this code, and since we determined the naming convention for the id, we also know that the paintIndex value will be the last value in a string of values that are delimited by -. If we split('-') that string and then pop() the last value off the end of it, we know that we're getting the paintIndex value.

class

class is also a great place to "embed" data - even if it doesn't map to any CSS class that's available to the script. If you're familiar with jQuery UI, you've probably seen many instances where class is used as a type of "switch" that doesn't actually drive CSS styles. Instead, it tells the JavaScript code what to do.

Of course, in JSX we don't use class. We use className. So that solution would look like this:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            className={`${paintIndex}`}
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={handleCellClick}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

And the event handler looks like this:

const handleCellClick = (event = {}) => {
   uiState.toggleHighlightedColor(event.target.className);
}
Enter fullscreen mode Exit fullscreen mode

Just as we previously grabbed paintIndex from the event object's id field, we're now grabbing it from className. How would this work if you also had "real" CSS classes in the className property? That would look like this:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            className={`cell ${paintIndex}`}
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={handleCellClick}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

And the event handler would look like this:'

const handleCellClick = (event = {}) => {
   const paintIndex = event.target.className.split(' ').pop();
   uiState.toggleHighlightedColor(paintIndex);
}
Enter fullscreen mode Exit fullscreen mode

The "cell" class on the <td> is a "real" class - meaning that it maps to predefined CSS properties. But we also embedded the paintIndex value into the className property and we extracted it by splitting the string on empty spaces.

To be fair, this approach may feel a bit... "brittle". Because it depends upon the paintIndex value being the last value in the space-delimited className string. If another developer came in and added another CSS class to the end of the className field, like this:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            className={`cell ${paintIndex} anotherCSSClass`}
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={handleCellClick}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

The logic would break. Because the event handler would grab anotherCSSClass off the end of the string - and try to treat it like it's the paintIndex. If you'd like to make it a bit more robust, you can change the logic to something like this:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            className={`cell paintIndex-${paintIndex} anotherCSSClass`}
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={handleCellClick}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

And then update the event handler like this:

const handleCellClick = (event = {}) => {
   const paintIndex = event.target.className
      .split(' ')
      .find(className => className.includes('paintIndex-'))
      .split('-')
      .pop();
   uiState.toggleHighlightedColor(paintIndex);
}
Enter fullscreen mode Exit fullscreen mode

By doing it this way, the value that's extracted for paintIndex isn't dependent upon being the last item in the space-delimited string. It can exist anywhere inside the className property, as long as it's prepended with "paintIndex-".


Image description

Why Should You Care?

To be frank, in small apps, having something like this isn't exactly a federal crime:

const getTableCells = (cells = [rgbModel], rowIndex = -1) => {
   return cells.map((cell, cellIndex) => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return (
         <td
            key={`cell-${rowIndex}-${cellIndex}`}
            onClick={paintIndex => handleCellClick(paintIndex)}
         >
            {paintIndex}
         </td>
      );
   })
}
Enter fullscreen mode Exit fullscreen mode

The performance "hit" you incur by defining a new function definition inside the onClick property is... minimal. In some cases, trying to "fix" it could be understandably defined as a "micro-optimization". But I do believe it's a solid practice to get in the habit of avoiding these whenever possible.

When the event handler doesn't need to have information passed into it from the clicked element, it's a no-brainer to keep arrow functions out of your event properties. But when it does require element-specific info, too often I see people blindly fall back on the easy method of dropping arrow functions into their properties. But there are many ways to avoid this - and they require little additional effort.

Top comments (13)

Collapse
 
starkraving profile image
Mike Ritchie

This approach can be improved by using data attributes in the target element handled by the click event. For example

const handleClick = (event) => {
  const paintIndex = event.target.dataset.paintindex;
  // do something with the value
}
<td data-paintindex={paintIndex} onClick={handleClick}>{paintIndex}</td>
Enter fullscreen mode Exit fullscreen mode

Data attributes can be applied to any element and are guaranteed never to interfere with native functionality provided by native attributes. Note that child tags on the element will mean that you may have to traverse up the parent tree back to the intended target before you can access the attributes. But it’s nice because you can have as many attributes as you need without trying to figure out what native attribute to use.

Great article!

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Great point! And yeah, I should've touched on data attributes as well. Thank you!

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡

Just did a quick cross-read (bookmarked for later fully-comprehensive reading) and I'm wondering if Styled-Components wouldn't apply better to this use-case (so you just pass the color as prop and you don't need to generate all classNames beforehand/through a process) 😁

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

That's basically what I covered under the "Wrapper Components" section.

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡

Amazing!
See, that's why I need to read it calmly afterwards πŸ˜‚πŸ˜‚ cross-reading is never enough πŸ˜…

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

LOL. To be fair, I didn't specifically put it in the context of styled components. But it's basically the same concept.

Thread Thread
 
joelbonetr profile image
JoelBonetR πŸ₯‡

haha yes, I've seen it briefly 😁 ty!

Thread Thread
 
400shynimol profile image
shynimol 400

Just did a quick cross-read (bookmarked for later fully-comprehensive reading) and I'm wondering if Styled-Components wouldn't apply better to this use-case (so you just pass the color as prop and you don't need to generate all classNames beforehand/through a process) 😁

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

There's a whole section on "Wrapper Components"...

Collapse
 
krlz profile image
krlz

Wow, this article is awesome! The way you explain the technique for extracting bits of data from JSX elements is on point. I'm pumped to see more content like this from you in the future! Keep up the great work!

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Thank you!

Collapse
 
mstewartgallus profile image
Molly Stewart-Gallus • Edited

You could also use data as well as data attributes. I mostly use data attributes for CSS.

For this specific case it's also possible to use useMemo.

const TableCells = ({cells}) => useMemo(() => cells.map(cell => {
      const paintIndex = colors.findIndex(color => color.name === cell.name);
      return {
        paintIndex,
        onClick(e) {
          return handleCellClick(paintIndex);
      };
}), [cells])
.map(({ paintIndex, onClick }) =>
<td onClick={onClick)>
  {paintIndex}
</td>
);
~~~
Enter fullscreen mode Exit fullscreen mode

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more