I\'m looking for a way to detect if a click event happened outside of a component, as described in this article. jQuery closest() is used to see if the target from a click e
I like the provided solutions that use to do the same thing by creating a wrapper around the component.
Since this is more of a behavior I thought of Strategy and came up with the following.
I'm new with React and I need a bit of help in order to save some boilerplate in the use cases
Please review and tell me what you think.
import ReactDOM from 'react-dom';
export default class ClickOutsideBehavior {
constructor({component, appContainer, onClickOutside}) {
// Can I extend the passed component's lifecycle events from here?
this.component = component;
this.appContainer = appContainer;
this.onClickOutside = onClickOutside;
}
enable() {
this.appContainer.addEventListener('click', this.handleDocumentClick);
}
disable() {
this.appContainer.removeEventListener('click', this.handleDocumentClick);
}
handleDocumentClick = (event) => {
const area = ReactDOM.findDOMNode(this.component);
if (!area.contains(event.target)) {
this.onClickOutside(event)
}
}
}
import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';
export default class AddCardControl extends Component {
constructor() {
super();
this.state = {
toggledOn: false,
text: ''
};
this.clickOutsideStrategy = new ClickOutsideBehavior({
component: this,
appContainer: APP_CONTAINER,
onClickOutside: () => this.toggleState(false)
});
}
componentDidMount () {
this.setState({toggledOn: !!this.props.toggledOn});
this.clickOutsideStrategy.enable();
}
componentWillUnmount () {
this.clickOutsideStrategy.disable();
}
toggleState(isOn) {
this.setState({toggledOn: isOn});
}
render() {...}
}
I thought of storing the passed component
lifecycle hooks and override them with methods simillar to this:
const baseDidMount = component.componentDidMount;
component.componentDidMount = () => {
this.enable();
baseDidMount.call(component)
}
component
is the component passed to the constructor of ClickOutsideBehavior
.
This will remove the enable/disable boilerplate from the user of this behavior but it doesn't look very nice though
If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick.
The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.
Here is the solution that best worked for me without attaching events to the container:
Certain HTML elements can have what is known as "focus", for example input elements. Those elements will also respond to the blur event, when they lose that focus.
To give any element the capacity to have focus, just make sure its tabindex attribute is set to anything other than -1. In regular HTML that would be by setting the tabindex
attribute, but in React you have to use tabIndex
(note the capital I
).
You can also do it via JavaScript with element.setAttribute('tabindex',0)
This is what I was using it for, to make a custom DropDown menu.
var DropDownMenu = React.createClass({
getInitialState: function(){
return {
expanded: false
}
},
expand: function(){
this.setState({expanded: true});
},
collapse: function(){
this.setState({expanded: false});
},
render: function(){
if(this.state.expanded){
var dropdown = ...; //the dropdown content
} else {
var dropdown = undefined;
}
return (
<div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
<div className="currentValue" onClick={this.expand}>
{this.props.displayValue}
</div>
{dropdown}
</div>
);
}
});
import { useClickAway } from "react-use";
useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
Hook implementation based on Tanner Linsley's excellent talk at JSConf Hawaii 2020:
useOuterClick
Hook APIconst Client = () => {
const innerRef = useOuterClick(ev => {/*what you want to do on outer click*/});
return <div ref={innerRef}> Inside </div>
};
function useOuterClick(callback) {
const callbackRef = useRef(); // initialize mutable callback ref
const innerRef = useRef(); // returned to client, who sets the "border" element
// update callback on each render, so second useEffect has most recent callback
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
function handleClick(e) {
if (innerRef.current && callbackRef.current &&
!innerRef.current.contains(e.target)
) callbackRef.current(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
Here is a working example:
/*
Custom Hook
*/
function useOuterClick(callback) {
const innerRef = useRef();
const callbackRef = useRef();
// set current callback in ref, before second useEffect uses it
useEffect(() => { // useEffect wrapper to be safe for concurrent mode
callbackRef.current = callback;
});
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
// read most recent callback and innerRef dom node from refs
function handleClick(e) {
if (
innerRef.current &&
callbackRef.current &&
!innerRef.current.contains(e.target)
) {
callbackRef.current(e);
}
}
}, []); // no need for callback + innerRef dep
return innerRef; // return ref; client can omit `useRef`
}
/*
Usage
*/
const Client = () => {
const [counter, setCounter] = useState(0);
const innerRef = useOuterClick(e => {
// counter state is up-to-date, when handler is called
alert(`Clicked outside! Increment counter to ${counter + 1}`);
setCounter(c => c + 1);
});
return (
<div>
<p>Click outside!</p>
<div id="container" ref={innerRef}>
Inside, counter: {counter}
</div>
</div>
);
};
ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>
useOuterClick
makes use of mutable refs to create a lean API for the Client
. A callback can be set without having to memoize it via useCallback
. The callback body still has access to the most recent props and state (no stale closure values).
iOS treats only certain elements as clickable. To circumvent this behavior, choose a different outer click listener than document
- nothing upwards including body
. E.g. you could add a listener on the React root div
in above example and expand its height (height: 100vh
or similar) to catch all outside clicks. Source: quirksmode.org
If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick. It captures events outside of a react component or jsx element.
The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.