Embedding Data Into React/JSX Elements
Adam Nathaniel Davis
Posted on March 22, 2023
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.
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.
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.
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}}>
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, id
s 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 id
s 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-".
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 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.
Posted on March 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 2, 2024