javascriptreactjscustom-data-attributehigher-order-componentsreact-hoc

React HOC: Pass data attributes to the first child/element of wrapped component


I have a hoc component like this:

export const withAttrs = (WrappedComponent) => {
  const ModifiedComponent = (props) => (
    <WrappedComponent {...props} data-test-id="this-is-a-element" />
  );

  return ModifiedComponent;
};

export default withAttrs;

and I use it like this:

import React from 'react';
import withAttrs from './withAttrs';

const SomeLink = () => <a><p>hey</p</a>;

export default withAttrs(SomeLink);

I expect to have an anchor tag like this:

<a data-test-id="this-is-a-element"><p>hey</p></a>

But the hoc doesn't add the data-attribute to the first element. Is there a way to achieve this?


Solution

  • But the hoc doesn't add the data-attribute to the first element.

    It's not the HOC that isn't adding it, it's SomeLink, which doesn't do anything with the props the HOC passes to it.

    The simple answer is to update SomeLink:

    const SomeLink = (props) => <a {...props}><p>hey</p></a>;
    

    That's by far the better thing to do than the following.

    If you can't do that, you could make your HOC add the property after the fact, but it seems inappropriate to have the HOC reach inside the component and change things. In fact, React makes the element objects it creates immutable, which strongly suggests you shouldn't try to mess with them.

    Still, it's possible, it's probably just a bad idea:

    export const withAttrs = (WrappedComponent) => {
        const ModifiedComponent = (props) => {
            // Note we're *calling* the function, not just putting it in
            // a React element via JSX; we're using it as a subroutine of
            // this component rather than as its own component.
            // This will only work with function components. (You could
            // write a version that handles class components as well,
            // but offhand I don't think you can make one HOC that handles
            // both in this case.)
            const result = WrappedComponent(props);
            return {
                ...result,
                props: {
                    ...result.props,
                    "data-test-id": "this-is-a-element",
                },
            };
        };
    
        return ModifiedComponent;
    };
    

    /*export*/ const withAttrs = (WrappedComponent) => {
        const ModifiedComponent = (props) => {
            // Note we're *calling* the function, not just putting it in
            // a React element via JSX; we're using it as a subroutine of
            // this component rather than as its own component.
            // This will only work with function components. (You could
            // write a version that handles class components as well,
            // but offhand I don't think you can make one HOC that handles
            // both in this case.)
            const result = WrappedComponent(props);
    
            // THIS IS PROBABLY A VERY BAD IDEA. React makes these objects
            // immutable, probably for a reason. We shouldn't be mucking
            // with them.
            return {
                ...result,
                props: {
                    ...result.props,
                    "data-test-id": "this-is-a-element",
                },
            };
        };
    
        return ModifiedComponent;
    };
    
    const SomeLink = () => <a><p>hey</p></a>;
    
    const SomeLinkWrapped = withAttrs(SomeLink);
    
    const Example = () => {
        return <div>
            <div>Unwrapped:</div>
            <SomeLink />
            <div>Wrapped:</div>
            <SomeLinkWrapped />
        </div>;
    };
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<Example />);
    /* So we can see that it was applied */
    [data-test-id=this-is-a-element] {
        color: green;
    }
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

    Again, I don't think I'd do that except as a very last resort, and I wouldn't be surprised if it breaks in future versions of React.