Mario Nikolaus
Mario Nikolaus

Reputation: 2406

React forwardRef HoC not giving reference to container element

I am trying to build a generic HOC for a closing element on click of outside its space(generic close on outside solution).

As I see it ,this could be achieved with forwardRef and HOC implementation and although there is an example in official docs I cannot seem to get it right.

So I want my HOC to create a reference to the container of component. It is wrapping because it has handlers to track clicks and act upon them. For instance, lets say we have a generic Dropdown component, one would expect that I can close it on any click outside the area of this component.

The code I currently have:

import React from 'react';

function withClose(Component) {
 class ClickContainer extends React.Component {
    constructor() {
      super();
      this.handleClose = this.handleClose.bind(this);
    }

    componentDidMount() {
      document.addEventListener('click', this.handleClose);
    }

    componentWillUnmount() {
      document.removeEventListener('click', this.handleClose);
    }

    handleClose(e) {
      // I expect having here context of container of wrapped component to do something like
      const { forwardedRef } = this.props; // <- I expect having context in forwardedRef variable
    }

    render() {
      const { forwardedRef, ...rest } = this.props;
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  return React.forwardRef((props, ref) => {
    return <ClickContainer {...props} forwardedRef={ref} />;
  });
}

export default withClose;

What am I missing here? I cannot make it work, I only get context of wrapped component not the element itself.

Thanks a bunch!

Upvotes: 5

Views: 7628

Answers (2)

Orar
Orar

Reputation: 958

Ref should be passed down to the element

Checkout https://codesandbox.io/s/7yzoqm747x

Assuming

export const Popup = (props,) => {
  const { name, forwardRef } = props;
  return (
   <div ref={forwardRef}>  // You need to pass it down from props
     {name}
   </div>
  )
}

And the HOC

export function withClose(Component) {
  class ClickContainer extends React.Component {
    constructor() {
     super();
     this.handleClose = this.handleClose.bind(this);
    }

    componentDidMount() {
      document.addEventListener('click', this.handleClose);
    }

    componentWillUnmount() {
      document.removeEventListener('click', this.handleClose);
    }

    handleClose(e) { 
     const { forwardRef } = this.props;
     console.log(forwardRef);
    }

    render() {
      const { forwardRef, ...rest } = this.props;
      return <Component forwardRef={forwardRef} {...rest} />;
    }
  }

  return React.forwardRef((props, ref) => {
    return <ClickContainer {...props} forwardRef={ref} />;
  });
}

And expect

 const CloseablePopup = withClose(Popup);

  class App extends Component {
    popupRef = React.createRef();
    render() {
      return (<CloseablePopup ref={popupRef} name="Closable Popup" />);
    }
  }

Upvotes: 3

tubu13
tubu13

Reputation: 934

I Implemented the same thing few days ago. I wanted a HOC that handle onClickoutside

I wanted that component to check on each click if the child was click or not and if it did to invoke a function on the child.

this was my solution:

import React from 'react';

export default function withClickOutside(WrappedComponent) {
  class WithClickOutside extends WrappedComponent {
    constructor(props) {
      super(props);
      this.handleClickOutside = this.handleClickOutside.bind(this);
      this.wrappedElement = React.createRef();
    }

    componentDidMount() {
      document.addEventListener('click', this.handleClickOutside, true);
    }

    componentWillUnmount() {
      document.removeEventListener('click', this.handleClickOutside, true);
    }


    handleClickOutside(event) {
       if (event.type !== 'click') return;
       if (!this.wrappedElement.current) {
          throw new Error(`No ref for element ${WrappedComponent.name}. Please create ref when using withClickOutside`);
        }

      if (!this.wrappedElement.current.contains(event.target)) {
         if (!this.onClickOutside) {
           throw new Error(`${WrappedComponent.name} does not implement onClickOutside function. Please create onClickOutside function when using withClickOutside`);
         }

       this.onClickOutside(event);
     }
   }


   render() {
     const wrapped = super.render();
     const element = React.cloneElement(
       wrapped,
       { ref: this.wrappedElement },
     );

     return element;
   }
 }

 return WithClickOutside;
}

Then the component you wrap must implement a function called onClickOutside .

Upvotes: 1

Related Questions