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 content originally appeared on DEV Community and was authored by Adam Nathaniel Davis

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

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

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

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

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

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: `${paintIndex}`}}>

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

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

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

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

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

And the event handler looks like this:

const handleCellClick = (event = {}) => {
   uiState.toggleHighlightedColor(event.target.className);
}

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

And the event handler would look like this:'

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

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

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

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

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

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


This content originally appeared on DEV Community and was authored by Adam Nathaniel Davis


Print Share Comment Cite Upload Translate Updates
APA

Adam Nathaniel Davis | Sciencx (2023-03-22T01:03:10+00:00) Embedding Data Into React/JSX Elements. Retrieved from https://www.scien.cx/2023/03/22/embedding-data-into-react-jsx-elements/

MLA
" » Embedding Data Into React/JSX Elements." Adam Nathaniel Davis | Sciencx - Wednesday March 22, 2023, https://www.scien.cx/2023/03/22/embedding-data-into-react-jsx-elements/
HARVARD
Adam Nathaniel Davis | Sciencx Wednesday March 22, 2023 » Embedding Data Into React/JSX Elements., viewed ,<https://www.scien.cx/2023/03/22/embedding-data-into-react-jsx-elements/>
VANCOUVER
Adam Nathaniel Davis | Sciencx - » Embedding Data Into React/JSX Elements. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/03/22/embedding-data-into-react-jsx-elements/
CHICAGO
" » Embedding Data Into React/JSX Elements." Adam Nathaniel Davis | Sciencx - Accessed . https://www.scien.cx/2023/03/22/embedding-data-into-react-jsx-elements/
IEEE
" » Embedding Data Into React/JSX Elements." Adam Nathaniel Davis | Sciencx [Online]. Available: https://www.scien.cx/2023/03/22/embedding-data-into-react-jsx-elements/. [Accessed: ]
rf:citation
» Embedding Data Into React/JSX Elements | Adam Nathaniel Davis | Sciencx | https://www.scien.cx/2023/03/22/embedding-data-into-react-jsx-elements/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.