Capturing a click outside a Reason-React Component
Tags: reason, react, tutorial, functional
Published: 2020-02-14 04:34:42

I found myself the other day trying to build a dropdown component in Reason that on outside click would close the menu if opened.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1ka3i4froq1enekjj26z.gif)

The only example I could find of this was in Reason from [GlennSL](https://github.com/glennsl/rescratch/blob/master/src/common/OnClickOutside.re) and it was a bit outdated since it was written in 2018, before hooks were fully implemented into reason. The code below is an updated version using hooks, it requires the `bs-dependency` of `bs-webapi` so make sure to add that before you use it.

```reasonml
[@genType "OnClickOutside"]
[@react.component]
let make = (~onOutsideClick, ~children) => {
  let outsideContainer = React.useRef(Js.Nullable.null);
  React.useEffect0(() => {
    open Webapi.Dom;
    let onClick = e => {
      let target = MouseEvent.target(e);
      let outsideDiv =
        Belt.Option.getExn(
          Js.Nullable.toOption(React.Ref.current(outsideContainer)),
        );
      let targetElement = EventTarget.unsafeAsElement(target);
      if (!Element.contains(targetElement, outsideDiv)) {
        onOutsideClick();
      };
    };
    Document.addClickEventListener(onClick, document);
    Some(() => Document.removeClickEventListener(onClick, document));
  });
  <div ref={outsideContainer->ReactDOMRe.Ref.domRef}> children </div>;
};
```

The logic breakdown:
- wrap the component in a div with a ref
- add an event listener for onClick
- in that onClick function check whether the element clicked contains the div with the ref, if it does then the click is inside the div. If not then the click is outside of the div.
- If the click is outside of the container then do whatever you need it to(in this case above, close the menu)

In the use case above we wrap our select component in the `OnClickOutside` and onOutsideClick close the dropdown menu container like so;
```reasonml
  let (visible, setVisibility) = React.useState(() => false);
  <OnClickOutside onOutsideClick={_e => setVisibility(_ => false)}>
      <Select.Button
          toggled=visible onClick={_e => setVisibility(_ => !visible)} /> 
      <Select.List name toggled=visible />
  </OnClickOutside>
```

If you enjoy writing Reason and would like to write more of it, you're in luck. Draftbit is [hiring](https://draftbit.com/about#jobs), we're a no-code tool that let's users build cross-platform apps and we're built mostly in reason! Check us out!

###UPDATE:
I was experiencing some state issues with the trigger so I added an extra useEffect to fix this.
```
  
[@genType "OverlayTrigger"]
[@react.component]
let make = (~onClick, ~children) => {
  let outsideContainer = React.useRef(Js.Nullable.null);
  open Webapi.Dom;
  let onClickHandler = event => {
    let target = MouseEvent.target(event);
    let outsideDiv =
      Belt.Option.getExn(
        Js.Nullable.toOption(React.Ref.current(outsideContainer)),
      );
    let targetElement = EventTarget.unsafeAsElement(target);
    if (!Element.contains(targetElement, outsideDiv)) {
      onClick();
    };
  };

  React.useEffect2(
    () => {
      Document.addMouseDownEventListener(onClickHandler, document);
      Some(
        () => Document.removeMouseDownEventListener(onClickHandler, document),
      );
    },
    (onClick, React.Ref.current(outsideContainer)),
  );

  <div
    className="cursor-pointer w-full"
    ref={outsideContainer->ReactDOMRe.Ref.domRef}>
    children
  </div>;
};
```