The Data-Attr Dilemma :
Data attributes can be incredibly useful, but in React they often feel like they go against the framework’s core patterns and philosophy. Still, I eventually ran into real problems where I had no choice but to use them.
So today, I want to talk about this little tension between data attributes and React, and explore when they actually do make sense.
A Quick Look at Data Attributes :
Let’s first do a quick refresher on data attributes in plain HTML , then we’ll dig into the real question: how (and why) React sometimes clashes with them.
In HTML, we can apply different attributes to any tag, and one of these is the data attribute. Any attribute that begins with data- is considered a data attribute. The part that comes after "-" is treated as the attribute’s key, and we can assign any custom value to it. For example:
<div data-id="50" data-something="something"></div>
The main purpose of data attributes is to store custom data directly on elements. In this example, the div element holds two pieces of information: an id and a something value. In other words, data attributes allow us to store non-visual, non-structural information inside an element.
There are two primary ways to access these values in JavaScript:
1.Using the getAttribute method:
In this case, we pass the full attribute name as a string to the method, and it returns the value of that attribute:
div.getAttribute('data-id'); // 50
div.getAttribute('data-something'); // something
2.Using the dataset object:
Every element has a dataset property, which contains a key–value list of all the data attributes defined on that element. For example:
div.dataset; // { id: '50', something: 'something' }
The important point here is that the keys inside dataset are exactly the parts that come after data- in the HTML. Accessing the values is also straightforward:
div.dataset.id;
div.dataset.something;
This is a quick overview of how data attributes work. They seem useful and pretty interesting, right?
However, when we step into the world of React, this simple concept doesn’t fit as cleanly into its philosophy.
React Already Has Your Data :
In React, data attributes are generally unpopular, and for good reason. Using them often feels like you don’t fully understand how React works.
React is all about generating the UI directly from your data. Anyone who has even a little experience with React knows that most of the time, you’re mapping over an array of data, rendering a component for each item, and passing the data down as props. Throughout this process, your UI is always a direct reflection of your data.
So where does that leave data attributes?
The short answer: we usually don’t need them at all. The data is already available because the UI itself is generated from it. Consider the example below:
A while ago, I read an article that showcased an interactive chart built using data attributes. I decided to try building a similar chart in React, but without using a single data attribute. And it worked perfectly.
Here’s the basic idea. I have an array of data like this:
const Data = [
{ id: "1", name: "Typescript", color: "#0046FF", percentage: 45 },
{ id: "2", name: "Javascript", color: "#FFC50F", percentage: 20 },
{ id: "3", name: "CSS", color: "#62109F", percentage: 25 },
{ id: "4", name: "HTML", color: "#FF8040", percentage: 10 },
];
I then used this array to generate two sets of components:
- Chart components showing the usage percentage and color of each language:
{Data.map((lang) => (
<div
onMouseLeave={() => setActiveLangId("")}
onMouseOver={() => setActiveLangId(lang.id)}
style={{
width: `${lang.percentage}%`,
backgroundColor: `${lang.color}`,
}}
className="langPercentage"
></div>
))}
- List components below the chart showing the name, color, and numeric percentage:
{Data.map((lang) => (
<Language
{...lang}
isActive={lang.id === activeLangId}
/>
))}
As you can see, the UI is generated directly from the data.
Now, suppose you want to do something when a user interacts with a component, for example, enlarging a list item when its corresponding chart bar is hovered. You already have full access to the data when defining the event handler:
onMouseOver={() => setActiveLangId(lang.id)}
No data attributes are necessary. The id of the language is available because the UI is created from the data itself.
The takeaway? In React, the DOM is just a rendered output. There isn't a reason to store extra data on the element itself, your components and state already hold everything you need.
The Scenario That Changed My Mind :
I used to believe that there was almost never a reason to use data attributes in React. But a while ago, while working on a frontend task, I discovered a situation where I had no real alternative.
The Scenario :
I had a scrollable container filled with cards, each displaying a substantial amount of data. My goal was: whenever a card reached the top of the container (and remained there until the next card arrived), the card’s title, and some additional minor information ,should appear in the container’s header.
The reasoning was simple: the user shouldn’t have to scroll back to the top of the card to see its title. Instead, when the card scrolls past the top, the header would display the title and some info until the next card takes its place.
The Challenge:
To implement this, I used the JavaScript Intersection Observer API . It worked perfectly to tell me when a card reaches the top of the container and when it fully exits. However, there was a problem:
The Intersection Observer gives you the target element (IntersectionObserverEntry.target) ,but it doesn’t give you the data that originally generated that element. Unlike normal event handlers, where you have access to the data when you define the handler, here you’re observing the DOM after the components have already been rendered, so the data isn’t directly accessible.
I initially thought I could query the DOM to find a p tag holding the title, but this felt brittle: any future change in the card’s internal structure would break the code.
The Solution: Data Attributes
Finally, I decided to store the card’s title directly in a data attribute on the card’s root element:
<div data-title="Card Title"> ... </div>
Now, when the Intersection Observer gave me the target element, I could easily access the title via dataset:
const title = entry.target.dataset.title;
setHeaderTitle(title);
This approach was simple, reliable, and future-proof.
Takeaways:
Most of the time in React, you don’t need data attributes, because the UI is generated directly from your data.
But in some cases, like Intersection Observer, Drag & Drop, or working with third-party DOM libraries, you only have access to the element itself. In these situations, data attributes are extremely useful.
If the dataset is large, you can store just an id in the data attribute, then look up the full data in your array.
Additionally, data attributes are commonly used for testing frameworks, such as data-testid, which is considered a normal and acceptable practice.
That’s my take on this tricky scenario with Intersection Observer in React. I’m curious, am I missing something? Is there a cleaner way to implement features like this without using data attributes, or is this truly one of those rare cases where they make sense? I’d love to hear your thoughts and experiences.

Top comments (0)