I’ve decided to take a break from random blogs and try to put something useful out. How useful remains to be seen, but still, useful is the goal here.
I am currently rewriting my dad’s business’s website in order to bring it out of a village in Punjab, in the early 2000’s, and into the present. Without getting into the nitty gritty, I decided to go with a tech stack I am most efficient at implementing. This is as follows: Typescript, NextJS (front/backend), StrapiJS (CMS). It’s simple, but it works.
Anyways, while developing I encountered a situation I haven’t thought about before when using React, dynamic and flexible HTML data attributes. Pretty simple on the face of it, they’re just HTML attributes. What’s the big deal? Nothing but, as with everything React, you need to consider them in the context of React.
Explicit Props Much?
As you will most likely be aware, React likes specificity. React and Typescript together are absolutely ballsing mental about it. This in and of itself isn’t an issue. Define the prop and be done. Right? Kinda.
In most cases defining explicit props is not only enough but is the best practice scenario. However, what about when you are making a highly reusable component such as a button or an anchor link? What about a scenario whereby you are developing a library? The list of scenarios where you are going to need flexibility goes on.
Prop Gymnastics?
So, in our situation we want to be able to be able to pass unknown and dynamic HTML attributes to our component. We also want our attributes to follow the correct syntax. The syntax essentially being that the attribute key must start with the word data
and be followed by a hyphenated list of strings containing lowercase letters.
For this we are going to need to define a few moving parts: our type definitions, a utility function to convert an array of objects to a single spreadable object and our component. Once we have all three we can combine them. Anyway, enough chatting fraff. Let’s get to it.
Our Type Definitions
This is pretty simple if I am honest, we just export a simple type with two keys key
and value
. Both take a string as their value. Our other type isn’t essential but it makes life easier so… meh.
Anyway, some code:
/** Generic data attributes to set on components */ export type DataAttribute = { /** The key for the attribute */ key: string; /** The value */ value: string; }; /** An array of data attributes */ export type DataAttributes = DataAttribute[];
Our Utility Function
So our types are now sorted. We need to define our magical utility function which will turn our array of key/value pairs into a single object which we can spread onto our component. This, again, is a simple function but it works, seemingly.
import { DataAttributes } from "@/types/component"; /** * Take an array of data attribute key/value pairs and return an object which * can be spread into a component's definition * * @param attributes * @returns A spreadable object of data-attributes */ export const dataAttrArrayToObject = ( attributes: DataAttributes ): Record=> attributes.reduce((output: Record<string, string>, current) => { // Ensure we follow the correct syntax so we will just return the output if not if (!/^data-([a-z]+-)*[a-z]+$/g.test(current.key)) return output; // Assign the key/value pair to the output object output[current.key] = current.value; // return the object return output; }, {});
As you can see this is an insanely simple reducer function which iterates over the array of data attributes, uses some simple regex to test whether the current key matches our required syntax and then assigns it to the output object if so.
Onto the Component
Types, check. Utility function, check. Last stop is the component itself. For this example we will just make a simple button component which is not going to be of any use in the real world but does demonstrate the use of our dynamic props.
The basic gist of what we are doing here is pretty simple. First we import our dependencies, then we define a type for our component. Then, within the component, we use our utility function to convert the array of key/value pairs into a single spreadable object. Finally, we return the button and spread the data attributes. Simples.
import { DataAttributes } from "@/types/component"; import { dataAttrArrayToObject } from "@/utils/dataAttributeArrayToObject"; // Component prop definition type ButtonProps = { dataAttributes: DataAttributes; }; const Button: React.FC= ({ dataAttributes }) => { // Use our utility function to get a spreadable object of attributes const attributesToSpread = dataAttrArrayToObject(dataAttributes); return ( <button onClick={() => console.log("click")} {...attributesToSpread}> Click </button> ); }; export { Button };
Conclusion
To round it up we first defined a DataAttribute type which has a key for key and a value, both requiring strings. Next we define a utility function to take an array of DataAttributes and reduce it into a single spreadable object. We then define a simple component which takes an array of DataAttributes as a prop then calls our utility function. Finally we return our button element and spread the object returned from our function.
This is a pretty simple article and a super simple solution but it is one I found worked for me. It is a situation I haven’t encountered before so I thought I would share it.
Peace, love and happiness.