Universal solution for data attributes in React

Viewed 636

I have some duplicating code across my project that looks like this:

function MyComp({prop1, prop2, ...restProps}) {
    
    // ...

    const dataAttributes = Object.keys(restProps).reduce((acc, key) => {
      if (key.startsWith('data-')) {
        acc[key] = restProps[key];
      }

      return acc;
    }, {});

    return (
        <div {...dataAttributes}>
            {/* ... */}
        </div>
    );
}

I'm thinking about automation of this behaviour (pass props that start with data- to the root element of the component if it's an html element).

Any thoughts?

3 Answers

Maybe you just can create a custom component for the html tags you use, like RichDiv, RichSpan... It would be like:

const getDataAttributes = (inputProps = {}) => Object.keys(inputProps).reduce((acc, key) => {
      if (key.startsWith('data-')) {
        acc[key] = inputProps[key];
      }

      return acc;
    }, {});

function RichDiv({children, className, ...restProps}) {
    const dataAttributes = getDataAttributes(restProps);

    return (
        <div {...dataAttributes} className={className}>
            {children}
        </div>
    );
}

function RichSpan({children, className, ...restProps}) {
    const dataAttributes = getDataAttributes(restProps);

    return (
        <span {...dataAttributes} className={className}>
            {children}
        </span>
    );
}

You can add any other whitelisted prop like I did with className to the custom components or even improve the props filter function you made to make it return both data- attributes and other HTML common attributes. Then, you just import and use your RichDiv instead of vanilla div and so.

The first and not very complex step might be the extraction of reducing part to a separate utility function like this:

export function extractDataAttributes(props) {
    return Object.keys(props).reduce((acc, key) => {
        if (key.startsWith("data-")) {
            acc[key] = props[key];
        }

        return acc;
    }, {});
}

Then you can use it in the following shorter way:

function MyComp({prop1, prop2, ...restProps}) {
    // ...
  
    return (
        <div {...extractDataAttributes(restProps)}>
            {/* ... */}
        </div>
    );
}

By doing so, you'll also explicitly mark that the resulting markup has data-attributes. On the other hand, there will be no unnecessary details.

If you find this solution insufficient at some point, it might be a good idea to use HOC looking inside its children and updating them. However, this approach might lower the explicitness of your code.

you could create a HOC function, where you can pass the remaining props to Component:

const withDataAttribute = (Component) => (props) => {
  let passThroughProps = { ...props }
  const dataAttributes = Object.keys(passThroughProps).reduce((acc, key) => {
    if (key.startsWith('data-')) {
      acc[key] = passThroughProps[key];
      const { [key]: any, ...rest } = passThroughProps
      passThroughProps = rest
    }

    return acc;
  }, {});

  return (
    <div {...dataAttributes}>
      <Component {...passThroughProps} />
    </div>
  );
}
Related