Detect click outside React component - javascript

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 event has the dom element as one of its parents. If there is a match the click event belongs to one of the children and is thus not considered to be outside of the component.
So in my component, I want to attach a click handler to the window. When the handler fires I need to compare the target with the dom children of my component.
The click event contains properties like "path" which seems to hold the dom path that the event has traveled. I'm not sure what to compare or how to best traverse it, and I'm thinking someone must have already put that in a clever utility function... No?

The following solution uses ES6 and follows best practices for binding as well as setting the ref through a method.
To see it in action:
Hooks Implementation
Class Implementation After React 16.3
Class Implementation Before React 16.3
Hooks Implementation:
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef);
return <div ref={wrapperRef}>{props.children}</div>;
}
Class Implementation:
After 16.3
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.wrapperRef = React.createRef();
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.wrapperRef}>{this.props.children}</div>;
}
}
Before 16.3
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Set the wrapper ref
*/
setWrapperRef(node) {
this.wrapperRef = node;
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.setWrapperRef}>{this.props.children}</div>;
}
}

I was stuck on the same issue. I am a bit late to the party here, but for me this is a really good solution. Hopefully it will be of help to someone else. You need to import findDOMNode from react-dom
import ReactDOM from 'react-dom';
// ... ✂
componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true);
}
handleClickOutside = event => {
const domNode = ReactDOM.findDOMNode(this);
if (!domNode || !domNode.contains(event.target)) {
this.setState({
visible: false
});
}
}
React Hooks Approach (16.8 +)
You can create a reusable hook called useComponentVisible.
import { useState, useEffect, useRef } from 'react';
export default function useComponentVisible(initialIsVisible) {
const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
const ref = useRef(null);
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setIsComponentVisible(false);
}
};
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
return { ref, isComponentVisible, setIsComponentVisible };
}
Then in the component you wish to add the functionality to do the following:
const DropDown = () => {
const { ref, isComponentVisible } = useComponentVisible(true);
return (
<div ref={ref}>
{isComponentVisible && (<p>Dropdown Component</p>)}
</div>
);
}
Find a codesandbox example here.

2021 Update:
It has bee a while since I added this response, and since it still seems to garner some interest, I thought I would update it to a more current React version. On 2021, this is how I would write this component:
import React, { useState } from "react";
import "./DropDown.css";
export function DropDown({ options, callback }) {
const [selected, setSelected] = useState("");
const [expanded, setExpanded] = useState(false);
function expand() {
setExpanded(true);
}
function close() {
setExpanded(false);
}
function select(event) {
const value = event.target.textContent;
callback(value);
close();
setSelected(value);
}
return (
<div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} >
<div>{selected}</div>
{expanded ? (
<div className={"dropdown-options-list"}>
{options.map((O) => (
<div className={"dropdown-option"} onClick={select}>
{O}
</div>
))}
</div>
) : null}
</div>
);
}
Original Answer (2016):
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>
);
}
});

After trying many methods here, I decided to use github.com/Pomax/react-onclickoutside because of how complete it is.
I installed the module via npm and imported it into my component:
import onClickOutside from 'react-onclickoutside'
Then, in my component class I defined the handleClickOutside method:
handleClickOutside = () => {
console.log('onClickOutside() method called')
}
And when exporting my component I wrapped it in onClickOutside():
export default onClickOutside(NameOfComponent)
That's it.

Hook implementation based on Tanner Linsley's excellent talk at JSConf Hawaii 2020:
useOuterClick API
const Client = () => {
const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
return <div ref={innerRef}> Inside </div>
};
Implementation
function useOuterClick(callback) {
const callbackRef = useRef(); // initialize mutable ref, which stores callback
const innerRef = useRef(); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
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>
Key points
useOuterClick makes use of mutable refs to provide lean Client API
stable click listener for lifetime of containing component ([] deps)
Client can set callback without needing to memoize it by useCallback
callback body has access to the most recent props and state - no stale closure values
(Side note for iOS)
iOS in general treats only certain elements as clickable. To make outer clicks work, choose a different click listener than document - nothing upwards including body. E.g. add a listener on the React root div and expand its height, like height: 100vh, to catch all outside clicks. Source: quirksmode.org

[Update] Solution with React ^16.8 using Hooks
CodeSandbox
import React, { useEffect, useRef, useState } from 'react';
const SampleComponent = () => {
const [clickedOutside, setClickedOutside] = useState(false);
const myRef = useRef();
const handleClickOutside = e => {
if (!myRef.current.contains(e.target)) {
setClickedOutside(true);
}
};
const handleClickInside = () => setClickedOutside(false);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
});
return (
<button ref={myRef} onClick={handleClickInside}>
{clickedOutside ? 'Bye!' : 'Hello!'}
</button>
);
};
export default SampleComponent;
Solution with React ^16.3:
CodeSandbox
import React, { Component } from "react";
class SampleComponent extends Component {
state = {
clickedOutside: false
};
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
myRef = React.createRef();
handleClickOutside = e => {
if (!this.myRef.current.contains(e.target)) {
this.setState({ clickedOutside: true });
}
};
handleClickInside = () => this.setState({ clickedOutside: false });
render() {
return (
<button ref={this.myRef} onClick={this.handleClickInside}>
{this.state.clickedOutside ? "Bye!" : "Hello!"}
</button>
);
}
}
export default SampleComponent;

None of the other answers here worked for me. I was trying to hide a popup on blur, but since the contents were absolutely positioned, the onBlur was firing even on the click of inner contents too.
Here is an approach that did work for me:
// Inside the component:
onBlur(event) {
// currentTarget refers to this component.
// relatedTarget refers to the element where the user clicked (or focused) which
// triggered this event.
// So in effect, this condition checks if the user clicked outside the component.
if (!event.currentTarget.contains(event.relatedTarget)) {
// do your thing.
}
},
Hope this helps.

I found a solution thanks to Ben Alpert on discuss.reactjs.org. The suggested approach attaches a handler to the document but that turned out to be problematic. Clicking on one of the components in my tree resulted in a rerender which removed the clicked element on update. Because the rerender from React happens before the document body handler is called, the element was not detected as "inside" the tree.
The solution to this was to add the handler on the application root element.
main:
window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)
component:
import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
export default class ClickListener extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
onClickOutside: PropTypes.func.isRequired
}
componentDidMount () {
window.__myapp_container.addEventListener('click', this.handleDocumentClick)
}
componentWillUnmount () {
window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
}
/* using fat arrow to bind to instance */
handleDocumentClick = (evt) => {
const area = ReactDOM.findDOMNode(this.refs.area);
if (!area.contains(evt.target)) {
this.props.onClickOutside(evt)
}
}
render () {
return (
<div ref='area'>
{this.props.children}
</div>
)
}
}

MUI has a small component to solve this problem: https://mui.com/base/react-click-away-listener/ that you can cherry-pick it. It weights below 1 kB gzipped, it supports mobile, IE 11, and portals.

The Ez way... (UPDATED 2022)
Create a hook: useOutsideClick.ts
export function useOutsideClick(ref: any, onClickOut: () => void){
useEffect(() => {
const onClick = ({target}: any) => !ref.contains(target) && onClickOut?.()
document.addEventListener("click", onClick);
return () => document.removeEventListener("click", onClick);
}, []);
}
Add componentRef to your component and call useOutsideClick
export function Example(){
const componentRef = useRef();
useOutsideClick(componentRef.current!, () => {
// do something here
});
return (
<div ref={componentRef as any}> My Component </div>
)
}

Alternatively:
const onClickOutsideListener = () => {
alert("click outside")
document.removeEventListener("click", onClickOutsideListener)
}
...
return (
<div
onMouseLeave={() => {
document.addEventListener("click", onClickOutsideListener)
}}
>
...
</div>

with typescript
function Tooltip(): ReactElement {
const [show, setShow] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent): void {
if (ref.current && !ref.current.contains(event.target as Node)) {
setShow(false);
}
}
// Bind the event listener
document.addEventListener('mousedown', handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside);
};
});
return (
<div ref={ref}></div>
)
}

import { useClickAway } from "react-use";
useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

For those who need absolute positioning, a simple option I opted for is to add a wrapper component that is styled to cover the whole page with a transparent background. Then you can add an onClick on this element to close your inside component.
<div style={{
position: 'fixed',
top: '0', right: '0', bottom: '0', left: '0',
zIndex: '1000',
}} onClick={() => handleOutsideClick()} >
<Content style={{position: 'absolute'}}/>
</div>
As it is right now if you add a click handler on content, the event will also be propagated to the upper div and therefore trigger the handlerOutsideClick. If this is not your desired behavior, simply stop the event progation on your handler.
<Content style={{position: 'absolute'}} onClick={e => {
e.stopPropagation();
desiredFunctionCall();
}}/>
`

Here is my approach (demo - https://jsfiddle.net/agymay93/4/):
I've created special component called WatchClickOutside and it can be used like (I assume JSX syntax):
<WatchClickOutside onClickOutside={this.handleClose}>
<SomeDropdownEtc>
</WatchClickOutside>
Here is code of WatchClickOutside component:
import React, { Component } from 'react';
export default class WatchClickOutside extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
document.body.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
// remember to remove all events to avoid memory leaks
document.body.removeEventListener('click', this.handleClick);
}
handleClick(event) {
const {container} = this.refs; // get container that we'll wait to be clicked outside
const {onClickOutside} = this.props; // get click outside callback
const {target} = event; // get direct click event target
// if there is no proper callback - no point of checking
if (typeof onClickOutside !== 'function') {
return;
}
// if target is container - container was not clicked outside
// if container contains clicked target - click was not outside of it
if (target !== container && !container.contains(target)) {
onClickOutside(event); // clicked outside - fire callback
}
}
render() {
return (
<div ref="container">
{this.props.children}
</div>
);
}
}

This already has many answers but they don't address e.stopPropagation() and preventing clicking on react links outside of the element you wish to close.
Due to the fact that React has it's own artificial event handler you aren't able to use document as the base for event listeners. You need to e.stopPropagation() before this as React uses document itself. If you use for example document.querySelector('body') instead. You are able to prevent the click from the React link. Following is an example of how I implement click outside and close.
This uses ES6 and React 16.3.
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false,
};
this.insideContainer = React.createRef();
}
componentWillMount() {
document.querySelector('body').addEventListener("click", this.handleClick, false);
}
componentWillUnmount() {
document.querySelector('body').removeEventListener("click", this.handleClick, false);
}
handleClick(e) {
/* Check that we've clicked outside of the container and that it is open */
if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
e.preventDefault();
e.stopPropagation();
this.setState({
isOpen: false,
})
}
};
togggleOpenHandler(e) {
e.preventDefault();
this.setState({
isOpen: !this.state.isOpen,
})
}
render(){
return(
<div>
<span ref={this.insideContainer}>
<a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
</span>
<a href="/" onClick({/* clickHandler */})>
Will not trigger a click when inside is open.
</a>
</div>
);
}
}
export default App;

Typescript with Hooks
Note: I'm using React version 16.3, with React.createRef. For other versions use the ref callback.
Dropdown component:
interface DropdownProps {
...
};
export const Dropdown: React.FC<DropdownProps> () {
const ref: React.RefObject<HTMLDivElement> = React.createRef();
const handleClickOutside = (event: MouseEvent) => {
if (ref && ref !== null) {
const cur = ref.current;
if (cur && !cur.contains(event.target as Node)) {
// close all dropdowns
}
}
}
useEffect(() => {
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
});
return (
<div ref={ref}>
...
</div>
);
}

I did this partly by following this and by following the React official docs on handling refs which requires react ^16.3. This is the only thing that worked for me after trying some of the other suggestions here...
class App extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentWillMount() {
document.addEventListener("mousedown", this.handleClick, false);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClick, false);
}
handleClick = e => {
/*Validating click is made inside a component*/
if ( this.inputRef.current === e.target ) {
return;
}
this.handleclickOutside();
};
handleClickOutside(){
/*code to handle what to do when clicked outside*/
}
render(){
return(
<div>
<span ref={this.inputRef} />
</div>
)
}
}

Simply with ClickAwayListener from mui (material-ui):
<ClickAwayListener onClickAway={handleClickAway}>
{children}
<ClickAwayListener >
for more info you can check:https://mui.com/base/react-click-away-listener/

To extend on the accepted answer made by Ben Bud, if you are using styled-components, passing refs that way will give you an error such as "this.wrapperRef.contains is not a function".
The suggested fix, in the comments, to wrap the styled component with a div and pass the ref there, works.
Having said that, in their docs they already explain the reason for this and the proper use of refs within styled-components:
Passing a ref prop to a styled component will give you an instance of the StyledComponent wrapper, but not to the underlying DOM node. This is due to how refs work. It's not possible to call DOM methods, like focus, on our wrappers directly.
To get a ref to the actual, wrapped DOM node, pass the callback to the innerRef prop instead.
Like so:
<StyledDiv innerRef={el => { this.el = el }} />
Then you can access it directly within the "handleClickOutside" function:
handleClickOutside = e => {
if (this.el && !this.el.contains(e.target)) {
console.log('clicked outside')
}
}
This also applies for the "onBlur" approach:
componentDidMount(){
this.el.focus()
}
blurHandler = () => {
console.log('clicked outside')
}
render(){
return(
<StyledDiv
onBlur={this.blurHandler}
tabIndex="0"
innerRef={el => { this.el = el }}
/>
)
}

This is my way of solving the problem
I return a boolean value from my custom hook, and when this value changes (true if the click was outside of the ref that I passed as an arg), this way i can catch this change with an useEffect hook, i hope it's clear for you.
Here's a live example:
Live Example on codesandbox
import { useEffect, useRef, useState } from "react";
const useOutsideClick = (ref) => {
const [outsieClick, setOutsideClick] = useState(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (!ref.current.contains(e.target)) {
setOutsideClick(true);
} else {
setOutsideClick(false);
}
setOutsideClick(null);
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
return outsieClick;
};
export const App = () => {
const buttonRef = useRef(null);
const buttonClickedOutside = useOutsideClick(buttonRef);
useEffect(() => {
// if the the click was outside of the button
// do whatever you want
if (buttonClickedOutside) {
alert("hey you clicked outside of the button");
}
}, [buttonClickedOutside]);
return (
<div className="App">
<button ref={buttonRef}>click outside me</button>
</div>
);
}

Typescript + simplified version of #ford04's proposal:
useOuterClick API
const Client = () => {
const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
return <div ref={ref}> Inside </div>
};
Implementation
export default function useOuterClick<T extends HTMLElement>(callback: Function) {
const callbackRef = useRef<Function>(); // initialize mutable ref, which stores callback
const innerRef = useRef<T>(null); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", _onClick);
return () => document.removeEventListener("click", _onClick);
function _onClick(e: any): void {
const clickedOutside = !(innerRef.current?.contains(e.target));
if (clickedOutside)
callbackRef.current?.(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}

So I faced a similar problem but in my case the selected answer here wasn't working because I had a button for the dropdown which is, well, a part of the document. So clicking the button also triggered the handleClickOutside function. To stop that from triggering, I had to add a new ref to the button and this !menuBtnRef.current.contains(e.target) to the conditional. I'm leaving it here if someone is facing the same issue like me.
Here's how the component looks like now:
const Component = () => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const menuRef = useRef(null);
const menuBtnRef = useRef(null);
const handleDropdown = (e) => {
setIsDropdownOpen(!isDropdownOpen);
}
const handleClickOutside = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target) && !menuBtnRef.current.contains(e.target)) {
setIsDropdownOpen(false);
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, true);
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
};
}, []);
return (
<button ref={menuBtnRef} onClick={handleDropdown}></button>
<div ref={menuRef} className={`${isDropdownOpen ? styles.dropdownMenuOpen : ''}`}>
// ...dropdown items
</div>
)
}

My biggest concern with all of the other answers is having to filter click events from the root/parent down. I found the easiest way was to simply set a sibling element with position: fixed, a z-index 1 behind the dropdown and handle the click event on the fixed element inside the same component. Keeps everything centralized to a given component.
Example code
#HTML
<div className="parent">
<div className={`dropdown ${this.state.open ? open : ''}`}>
...content
</div>
<div className="outer-handler" onClick={() => this.setState({open: false})}>
</div>
</div>
#SASS
.dropdown {
display: none;
position: absolute;
top: 0px;
left: 0px;
z-index: 100;
&.open {
display: block;
}
}
.outer-handler {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
z-index: 99;
display: none;
&.open {
display: block;
}
}

componentWillMount(){
document.addEventListener('mousedown', this.handleClickOutside)
}
handleClickOutside(event) {
if(event.path[0].id !== 'your-button'){
this.setState({showWhatever: false})
}
}
Event path[0] is the last item clicked

I used this module (I have no association with the author)
npm install react-onclickout --save
const ClickOutHandler = require('react-onclickout');
class ExampleComponent extends React.Component {
onClickOut(e) {
if (hasClass(e.target, 'ignore-me')) return;
alert('user clicked outside of the component!');
}
render() {
return (
<ClickOutHandler onClickOut={this.onClickOut}>
<div>Click outside of me!</div>
</ClickOutHandler>
);
}
}
It did the job nicely.

UseOnClickOutside Hook - React 16.8 +
Create a general useOnOutsideClick function
export const useOnOutsideClick = handleOutsideClick => {
const innerBorderRef = useRef();
const onClick = event => {
if (
innerBorderRef.current &&
!innerBorderRef.current.contains(event.target)
) {
handleOutsideClick();
}
};
useMountEffect(() => {
document.addEventListener("click", onClick, true);
return () => {
document.removeEventListener("click", onClick, true);
};
});
return { innerBorderRef };
};
const useMountEffect = fun => useEffect(fun, []);
Then use the hook in any functional component.
const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {
const [open, setOpen] = useState(false);
const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));
return (
<div>
<button onClick={() => setOpen(true)}>open</button>
{open && (
<div ref={innerBorderRef}>
<SomeChild/>
</div>
)}
</div>
);
};
Link to demo
Partially inspired by #pau1fitzgerald answer.

In my DROPDOWN case the Ben Bud's solution worked well, but I had a separate toggle button with an onClick handler. So the outside clicking logic conflicted with the button onClick toggler. Here is how I solved it by passing the button's ref as well:
import React, { useRef, useEffect, useState } from "react";
/**
* Hook that triggers onClose when clicked outside of ref and buttonRef elements
*/
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
useEffect(() => {
function handleClickOutside(event) {
/* clicked on the element itself */
if (ref.current && !ref.current.contains(event.target)) {
return;
}
/* clicked on the toggle button */
if (buttonRef.current && !buttonRef.current.contains(event.target)) {
return;
}
/* If it's something else, trigger onClose */
onOutsideClick();
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function DropdownMenu(props) {
const wrapperRef = useRef(null);
const buttonRef = useRef(null);
const [dropdownVisible, setDropdownVisible] = useState(false);
useOutsideClicker(wrapperRef, buttonRef, closeDropdown);
const toggleDropdown = () => setDropdownVisible(visible => !visible);
const closeDropdown = () => setDropdownVisible(false);
return (
<div>
<button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
{dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
</div>
);
}

I had a similar use case where I had to develop a custom dropdown menu. it should close automatically when the user clicks outside. here is the recent React Hooks implementation-
import { useEffect, useRef, useState } from "react";
export const App = () => {
const ref = useRef();
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {
const checkIfClickedOutside = (e) => {
// If the menu is open and the clicked target is not within the menu,
// then close the menu
if (isMenuOpen && ref.current && !ref.current.contains(e.target)) {
setIsMenuOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}, [isMenuOpen]);
return (
<div className="wrapper" ref={ref}>
<button
className="button"
onClick={() => setIsMenuOpen((oldState) => !oldState)}
>
Click Me
</button>
{isMenuOpen && (
<ul className="list">
<li className="list-item">dropdown option 1</li>
<li className="list-item">dropdown option 2</li>
<li className="list-item">dropdown option 3</li>
<li className="list-item">dropdown option 4</li>
</ul>
)}
</div>
);
}

An example with Strategy
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.
ClickOutsideBehavior
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)
}
}
}
Sample Usage
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() {...}
}
Notes
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

Related

Value of variable outside of useEffect hook has old data

What the code does: It's performing a DOM search based on what's typed in an input (it's searching elements by text). All this is happening in a React component.
import { useEffect, useReducer } from "react";
let elements: any[] = [];
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
elements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
console.log('elements 1:', elements)
}, [state]);
console.log('elemets 2:', elements)
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
The problem: The value of elements outside of useEffect is the old data. For example, if you type a in the input, console.log('elements 1:', elements) will log 1, and console.log('elements 2:', elements) will log 0. Note: there are 3 buttons, and one of them has the text a.
The strange thing is that the problem doesn't happen if you comment out this if-statement:
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
In this case, if you type anything (since the pattern matching has been commented out), console.log('elements 1:', elements) and console.log('elements 2:', elements) will log 3. Note: there are 3 buttons.
Question: What could be the problem, and how to fix it? I want to render the current length of elements.
Live code:
It's happening because of the elements variable is not a state, so it's not reactive.
Create a state for the elements:
const [elements, setElements] = useState<HTMLButtonElement[]>([])
And use this state to handle the elements.
import { useEffect, useReducer, useState } from "react";
const App = () => {
const initialState = { keyEvent: {}, value: "Initial state" };
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates }),
initialState
);
const [elements, setElements] = useState<HTMLButtonElement[]>([])
function handleInputChange(event: any) {
updateState({ value: event.target.value });
}
function isCommand(event: KeyboardEvent) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event: any) {
if (isCommand(event)) {
updateState({ keyEvent: event });
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
useEffect(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
let newElements = Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
}
);
setElements(newElements)
console.log("elements 1:", elements?.length);
}, [state]);
console.log("elemets 2:", elements?.length);
return (
<div>
<input
id="input"
type="text"
onChange={handleInputChange}
value={state.value}
/>
<div id="count">{elements?.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
export default App;
Your useEffect() runs after your component has rendendered. So the sequence is:
You type something into input, that triggers handleInputChange
handleInputChange then updates your state using updateState()
The state update causes a rerender, so App is called App()
console.log('elemets 2:', elements.length) runs and logs elements as 0 as it's still empty
App returns the new JSX
Your useEffect() callback runs, updating elements
Notice how we're only updating the elements after you've rerendered and App has been called.
The state of your React app should be used to describe your UI in React. Since elements isn't React state, it has a chance of becoming out of sync with the UI (as you've seen), whereas using state doesn't have this issue as state updates always trigger a UI update. Consider making elements part of your state. If it needs to be accessible throughout your entire App, you can pass it down as props to children components, or use context to make it accessible throughout all your components.
With that being said, I would make the following updates:
Add elements to your state
Remove your useEffect() with the dependency of [state]. If we were to update the elements state within this effect, then that would trigger another rerender directly after the one we just did for the state update. This isn't efficient, and instead, we can tie the update directly to your event handler. See You Might Not Need an Effect for more details and dealing with other types of scenarios:
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useEffect, useReducer} = React;
const App = () => {
const initialState = {keyEvent: {}, value: "Initial state", elements: []};
const [state, updateState] = useReducer(
(state: any, updates: any) => ({ ...state, ...updates}),
initialState
);
function searchDOM(value) {
const selectors = "button";
const pattern = new RegExp(value === "" ? "^$" : value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
return nodeWithText?.textContent?.match(pattern);
}
return false;
}
);
}
function handleInputChange(event) {
updateState({
value: event.target.value,
elements: searchDOM(event.target.value)
});
}
function isCommand(event) {
return event.ctrlKey;
}
function handleDocumentKeyDown(event) {
if (isCommand(event)) {
updateState({
keyEvent: event
});
}
}
useEffect(() => {
document.addEventListener("keydown", handleDocumentKeyDown);
return () => {
document.removeEventListener("keydown", handleDocumentKeyDown);
};
}, []);
console.log("elements:", state.elements.length);
return (
<div>
<input id="input" type="text" onChange={handleInputChange} value={state.value} />
<div id="count">{state.elements.length}</div>
<button>a</button>
<button>b</button>
<button>c</button>
</div>
);
};
ReactDOM.createRoot(document.body).render(<App />);
</script>
useEffect triggered after react completed its render phase & flush the new changes to the DOM.
In your case you have two useEffects. The first one register your event lister which will then update your component state when input field change. This triggers a state update.( because of the setState )
So React will start render the component again & finish the cycle. And now you have 2nd useEffect which has state in dependency array. Since the state was updated & the new changes are committed to the DOM, react will execute 2nd useEffect logic.
Since your 2nd useEffect just assign some values to a normal variable React will not go re render your component again.
Based on your requirement you don't need a 2nd useEffect. You can use a useMemo,
let elements = useMemo(() => {
const selectors = "button";
const pattern = new RegExp(state.value === "" ? "^$" : state.value);
return Array.from(document.querySelectorAll(selectors)).filter(
(element) => {
if (element.childNodes) {
const nodeWithText = Array.from(element.childNodes).find(
(childNode) => childNode.nodeType === Node.TEXT_NODE
);
if (nodeWithText) {
// The delay won't happenn if you comment out this conditional statement:
if (nodeWithText.textContent?.match(pattern)) {
return element;
}
}
}
})
}, [state])
Note: You don't need to assign your elements into another state. It just create another unwanted re render in cycle. Since you are just doing a calculation to find out the element array you can do it with the useMemo

How to properly clean up open menu when navigating to next page

I have created simple but working dropdown-menu. In this example menu does not contain anything but text 'menu' and have no styles.
When "open menu"-button is clicked, app shows div that contains the menu, and adds eventListener to document click events. When eventListener is added and user click anywhere in document, function checks if click was happened inside the menu, and if so, does nothing. If click was outside the menu, it removes eventHandler and closes the menu.
Is there something wrong with this aproach? The major problem with this is that if menu is open when I click any link on page, I got this nasty react warning:
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I have added useEffect cleanup function to remove eventHandler if menu is open, but that does not help, I still got same error message.
Can you point me what I have done wrong?
const DropDown = () => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const closeMenu = (event: MouseEvent) => {
if (event.target && event.target instanceof HTMLElement && ref.current) {
if (ref.current.contains(event.target)) return;
}
document.removeEventListener('click', closeMenu);
setOpen(false);
};
const toggleMenu = () => {
if (!open) {
document.addEventListener('click', closeMenu);
setOpen(true);
} else {
document.removeEventListener('click', closeMenu);
setOpen(false);
}
};
useEffect(() => {
return () => {
if (open) {
document.removeEventListener('click', closeMenu);
setOpen(false);
}
};
}, [closeMenu]);
return (
<>
<button onClick={toggleMenu}>open menu</button>
{open && <div ref={ref}>menu</div>}
</>
);
};
export default DropDown;
I think the issue is caused by the setOpen(false); in the useEffect hook: the clean-up function is called when the component unmounts therefore it make no sense setting its state. For the same reason, the check on whether the menu is open or not is redundant; if the menu is unmounted, you can remove the event handler regardless.
Try:
const DropDown = () => {
const [open, setOpen] = useState(false);
const ref = useRef < HTMLDivElement > (null);
/*
*"useCallback()" hook will avoid unnecessary re-creating
* the function at each re-render
*/
const closeMenu = useCallback((event: MouseEvent) => {
if (event.target && event.target instanceof HTMLElement && ref.current) {
if (ref.current.contains(event.target)) return;
}
/*
* You do not need to remove the event handler here
* you create it when the component is mounted and
* remove it when it is unmounted via the "useEffect()" hook
*/
//document.removeEventListener('click', closeMenu);
setOpen(false);
}, []);
const toggleMenu = () => {
/*
* This is not necessary, you can do it in one line.
* Actually, you can entirely remove the function and
* call 'setOpen(!open)' directly from the button:
* <button onClick={() => {setOpen(!open)}}>
*/
//if (!open) {
// document.addEventListener('click', closeMenu);
// setOpen(true);
//} else {
// document.removeEventListener('click', closeMenu);
// setOpen(false);
//}
setOpen(!open);
};
useEffect(() => {
document.addEventListener('click', closeMenu);
return () => {
document.removeEventListener('click', closeMenu);
};
}, [closeMenu]);
return ( <
>
<
button onClick = {
toggleMenu
} > open menu < /button> {
open && < div ref = {
ref
} > menu < /div>} <
/>
);
};
export default DropDown;

Event Listener in react functional component

Essentially, I have this function component, but I'm struggling with adding the listener just to this component, since right now it's going to the entire window. I tried using useRef, but that wasn't working. Any ideas?
const AddReply =({item })=> {
const {replies, setReplies, artists, setArtists, messages, setMessages, songs, setSongs, posts, setPosts, likes, setLikes, user, setUser, accesstoken, setAccesToken, refreshtoken, setRefreshtoken} = React.useContext(InfoContext);
const refKey = useRef();
function eventhandler(e) {
if (e.code === 'Enter') {
handleSubmitReply();
console.log("Works");
}
}
function handleSubmitReply(){
console.log(document.getElementById("textareareply").value);
const likesref=dbLikes.push(0);
const likeskey=likesref.getKey();
console.log("Likes key is ", likeskey);
const ref = dbReplies.child(item['replies']).push({
'content':document.getElementById("textareareply").value,
'posterid': user.id,
'likes': likeskey,
"createdAt": {'.sv': 'timestamp'}
});
}
useEffect(() => {
// if I were to use useRef, then I tried using ref.current,
// but then I got that "TypeError: refKey.current.addEventListener
// is not a function".
window.addEventListener("keyup", eventhandler);
return () => {
window.removeEventListener("keyup", eventhandler);
};
}, []);
return(
<div>
<TextArea id="textareareply" ref={refKey} rows={1} placeholder='Reply to post' />
<Form.Button fluid positive onClick = {()=>handleSubmitReply()} style={{marginTop:"10px"}}>Reply</Form.Button>
</div>
)}
export default AddReply;```
Your useRef doesn't work probably because <TextArea> is a functional component so can't use ref. Instead, attach ref to the container:
<div ref={refKey}>
<TextArea id="textareareply" rows={1} placeholder='Reply to post' />
<Form.Button fluid positive onClick = {()=>handleSubmitReply()}
style={{marginTop:"10px"}}>Reply</Form.Button>
</div>
Then you can now attach event to it:
useEffect(() => {
const div = refKey.current;
div.addEventListener("keyup", eventhandler);
return () => {
div.removeEventListener("keyup", eventhandler);
};
}, []);
That should solve your problem, but if you still prefer the current way of listening on window without useRef, then just add logic to the event handler to act only on your component:
function eventhandler(e) {
if (e.target.getAttribute('id') === 'textareareply') {
if (e.code === 'Enter') {
console.log("Works");
}
}
}
When we use a functional component, we pass directly the function reference into DOM event attributes, such as:
function AddReply() {
function handleButtonClick(event) {
console.log(event)
}
return (
<button onClick={handleButtonClick}> Click Here </button>
);
}

Detect click outside component react hooks

I am trying to use react hooks to determine if a user has clicked outside an element. I am using useRef to get a reference to the element.
Can anyone see how to fix this. I am getting the following errors and following answers from here.
Property 'contains' does not exist on type 'RefObject'
This error above seems to be a typescript issue.
There is a code sandbox here with a different error.
In both cases it isn't working.
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
const Menu = () => {
const wrapperRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(true);
// below is the same as componentDidMount and componentDidUnmount
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
const handleClickOutside = event => {
const domNode = ReactDOM.findDOMNode(wrapperRef);
// error is coming from below
if (!domNode || !domNode.contains(event.target)) {
setIsVisible(false);
}
}
return(
<div ref={wrapperRef}>
<p>Menu</p>
</div>
)
}
the useRef API should be used like this:
import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const wrapperRef = useRef(null);
const [isVisible, setIsVisible] = useState(true);
// below is the same as componentDidMount and componentDidUnmount
useEffect(() => {
document.addEventListener("click", handleClickOutside, false);
return () => {
document.removeEventListener("click", handleClickOutside, false);
};
}, []);
const handleClickOutside = event => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
setIsVisible(false);
}
};
return (
isVisible && (
<div className="menu" ref={wrapperRef}>
<p>Menu</p>
</div>
)
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I have created this common hook, which can be used for all divs which want this functionality.
import { useEffect } from 'react';
/**
*
* #param {*} ref - Ref of your parent div
* #param {*} callback - Callback which can be used to change your maintained state in your component
* #author Pranav Shinde 30-Nov-2021
*/
const useOutsideClick = (ref, callback) => {
useEffect(() => {
const handleClickOutside = (evt) => {
if (ref.current && !ref.current.contains(evt.target)) {
callback(); //Do what you want to handle in the callback
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
});
};
export default useOutsideClick;
Usage -
Import the hook in your component
Add a ref to your wrapper div and pass it to the hook
add a callback function to change your state(Hide the dropdown/modal)
import React, { useRef } from 'react';
import useOutsideClick from '../../../../hooks/useOutsideClick';
const ImpactDropDown = ({ setimpactDropDown }) => {
const impactRef = useRef();
useOutsideClick(impactRef, () => setimpactDropDown(false)); //Change my dropdown state to close when clicked outside
return (
<div ref={impactRef} className="wrapper">
{/* Your Dropdown or Modal */}
</div>
);
};
export default ImpactDropDown;
Check out this library from Andarist called use-onclickoutside.
import * as React from 'react'
import useOnClickOutside from 'use-onclickoutside'
export default function Modal({ close }) {
const ref = React.useRef(null)
useOnClickOutside(ref, close)
return <div ref={ref}>{'Modal content'}</div>
}
An alternative solution is to use a full-screen invisible box.
import React, { useState } from 'react';
const Menu = () => {
const [active, setActive] = useState(false);
return(
<div>
{/* The menu has z-index = 1, so it's always on top */}
<div className = 'Menu' onClick = {() => setActive(true)}
{active
? <p> Menu active </p>
: <p> Menu inactive </p>
}
</div>
{/* This is a full-screen box with z-index = 0 */}
{active
? <div className = 'Invisible' onClick = {() => setActive(false)}></div>
: null
}
</div>
);
}
And the CSS:
.Menu{
z-index: 1;
}
.Invisible{
height: 100vh;
left: 0;
position: fixed;
top: 0;
width: 100vw;
z-index: 0;
}

Toggle Class based on scroll React JS

I'm using bootstrap 4 nav bar and would like to change the background color after ig 400px down scroll down. I was looking at the react docs and found a onScroll but couldn't find that much info on it. So far I have...
I don't know if I'm using the right event listener or how to set the height etc.
And I'm not really setting inline styles...
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = { scrollBackground: 'nav-bg' };
this.handleScroll = this.handleScroll.bind(this);
}
handleScroll(){
this.setState ({
scrollBackground: !this.state.scrollBackground
})
}
render() {
const scrollBg = this.scrollBackground ? 'nav-bg scrolling' : 'nav-bg';
return (
<div>
<Navbar inverse toggleable className={this.state.scrollBackground}
onScroll={this.handleScroll}>
...
</Navbar>
</div>
);
}
}
export default App;
For those of you who are reading this question after 2020, I've taken #glennreyes answer and rewritten it using React Hooks:
const [scroll, setScroll] = useState(0)
useEffect(() => {
document.addEventListener("scroll", () => {
const scrollCheck = window.scrollY < 100
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
})
})
Bear in mind that, useState has an array of two elements, firstly the state object and secondly the function that updates it.
Along the lines, useEffect helps us replace componentDidmount, the function written currently does not do any clean ups for brevity purposes.
If you find it essential to clean up, you can just return a function inside the useEffect.
You can read comprehensively here.
UPDATE:
If you guys felt like making it modular and even do the clean up, you can do something like this:
Create a custom hook as below;
import { useState, useEffect } from "react"
export const useScrollHandler = () => {
// setting initial value to true
const [scroll, setScroll] = useState(1)
// running on mount
useEffect(() => {
const onScroll = () => {
const scrollCheck = window.scrollY < 10
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
}
// setting the event handler from web API
document.addEventListener("scroll", onScroll)
// cleaning up from the web API
return () => {
document.removeEventListener("scroll", onScroll)
}
}, [scroll, setScroll])
return scroll
}
Call it inside any component that you find suitable:
const component = () => {
// calling our custom hook
const scroll = useScrollHandler()
....... rest of your code
}
One way to add a scroll listener is to use the componentDidMount() lifecycle method. Following example should give you an idea:
import React from 'react';
import { render } from 'react-dom';
class App extends React.Component {
state = {
isTop: true,
};
componentDidMount() {
document.addEventListener('scroll', () => {
const isTop = window.scrollY < 100;
if (isTop !== this.state.isTop) {
this.setState({ isTop })
}
});
}
render() {
return (
<div style={{ height: '200vh' }}>
<h2 style={{ position: 'fixed', top: 0 }}>Scroll {this.state.isTop ? 'down' : 'up'}!</h2>
</div>
);
}
}
render(<App />, document.getElementById('root'));
This changes the Text from "Scroll down" to "Scroll up" when your scrollY position is at 100 and above.
Edit: Should avoid the overkill of updating the state on each scroll. Only update it when the boolean value changes.
const [scroll, setScroll] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > specify_height_you_want_to_change_after_here);
});
}, []);
Then you can change your class or anything according to scroll.
<nav className={scroll ? "bg-black" : "bg-white"}>...</nav>
It's Better
import React from 'react';
import { render } from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isTop: true
};
this.onScroll = this.onScroll.bind(this);
}
componentDidMount() {
document.addEventListener('scroll', () => {
const isTop = window.scrollY < 100;
if (isTop !== this.state.isTop) {
this.onScroll(isTop);
}
});
}
onScroll(isTop) {
this.setState({ isTop });
}
render() {
return (
<div style={{ height: '200vh' }}>
<h2 style={{ position: 'fixed', top: 0 }}>Scroll {this.state.isTop ? 'down' : 'up'}!</h2>
</div>
);
}
}
render(<App />, document.getElementById('root'));
This is yet another take / my take on hooks approach for on scroll displaying and hiding of a random page element.
I have been very much inspired from: Dan Abramov's post here.
You can check a full working example, in this CodeSandbox demo.
The following is the code for the useScroll custom hook:
import React, { useState, useEffect } from "react";
export const useScroll = callback => {
const [scrollDirection, setScrollDirection] = useState(true);
const handleScroll = () => {
const direction = (() => {
// if scroll is at top or at bottom return null,
// so that it would be possible to catch and enforce a special behaviour in such a case.
if (
window.pageYOffset === 0 ||
window.innerHeight + Math.ceil(window.pageYOffset) >=
document.body.offsetHeight
)
return null;
// otherwise return the direction of the scroll
return scrollDirection < window.pageYOffset ? "down" : "up";
})();
callback(direction);
setScrollDirection(window.pageYOffset);
};
// adding and cleanning up de event listener
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
};
And this hook will be consumed like this:
useScroll(direction => {
setScrollDirection(direction);
});
A full component using this custom hook:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import CustomElement, { useScroll } from "./element";
import Scrollable from "./scrollable";
function Page() {
const [scrollDirection, setScrollDirection] = useState(null);
useScroll(direction => {
setScrollDirection(direction);
});
return (
<div>
{/* a custom element that implements some scroll direction behaviour */}
{/* "./element" exports useScroll hook and <CustomElement> */}
<CustomElement scrollDirection={scrollDirection} />
{/* just a lorem ipsum long text */}
<Scrollable />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Page />, rootElement);
And lastly the code for CustomElement:
import React, { useState, useEffect } from "react";
export default props => {
const [elementVisible, setElementVisible] = useState(true);
const { scrollDirection } = props;
// when scroll direction changes element visibility adapts, but can do anything we want it to do
// U can use ScrollDirection and implement some page shake effect while scrolling
useEffect(() => {
setElementVisible(
scrollDirection === "down"
? false
: scrollDirection === "up"
? true
: true
);
}, [scrollDirection]);
return (
<div
style={{
background: "#ff0",
padding: "20px",
position: "fixed",
width: "100%",
display: `${elementVisible ? "inherit" : "none"}`
}}
>
element
</div>
);
};
I have changed #PouyaAtaei answer a bit for my use case.
import { useState, useEffect } from "react"
// Added distance parameter to determine how much
// from the top tell return value is updated.
// The name of the hook better reflects intended use.
export const useHasScrolled = (distance = 10) => {
// setting initial value to false
const [scroll, setScroll] = useState(false)
// running on mount
useEffect(() => {
const onScroll = () => {
// Logic is false tell user reaches threshold, then true after.
const scrollCheck = window.scrollY >= distance;
if (scrollCheck !== scroll) {
setScroll(scrollCheck)
}
}
// setting the event handler from web API
document.addEventListener("scroll", onScroll)
// cleaning up from the web API
return () => {
document.removeEventListener("scroll", onScroll)
}
}, [scroll, setScroll])
return scroll
}
Calling the hook:
const component = () => {
// calling our custom hook and optional distance agument.
const scroll = useHasScrolled(250)
}
These are two hooks - one for direction (up/down/none) and one for the actual position
Use like this:
useScrollPosition(position => {
console.log(position)
})
useScrollDirection(direction => {
console.log(direction)
})
Here are the hooks:
import { useState, useEffect } from "react"
export const SCROLL_DIRECTION_DOWN = "SCROLL_DIRECTION_DOWN"
export const SCROLL_DIRECTION_UP = "SCROLL_DIRECTION_UP"
export const SCROLL_DIRECTION_NONE = "SCROLL_DIRECTION_NONE"
export const useScrollDirection = callback => {
const [lastYPosition, setLastYPosition] = useState(window.pageYOffset)
const [timer, setTimer] = useState(null)
const handleScroll = () => {
if (timer !== null) {
clearTimeout(timer)
}
setTimer(
setTimeout(function () {
callback(SCROLL_DIRECTION_NONE)
}, 150)
)
if (window.pageYOffset === lastYPosition) return SCROLL_DIRECTION_NONE
const direction = (() => {
return lastYPosition < window.pageYOffset
? SCROLL_DIRECTION_DOWN
: SCROLL_DIRECTION_UP
})()
callback(direction)
setLastYPosition(window.pageYOffset)
}
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
})
}
export const useScrollPosition = callback => {
const handleScroll = () => {
callback(window.pageYOffset)
}
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
})
}
how to fix :
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
MenuNews
const [scroll, setScroll] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > specify_height_you_want_to_change_after_here);
});
}, []);
Approach without scroll event listener
import { useEffect, useState } from "react";
interface Props {
elementId: string;
position: string;
}
const useCheckScrollPosition = ({ elementId, position }: Props) => {
const [isOverScrollPosition, setIsOverScrollPosition] = useState<boolean>(false);
useEffect(() => {
if (
"IntersectionObserver" in window &&
"IntersectionObserverEntry" in window &&
"intersectionRatio" in window.IntersectionObserverEntry.prototype
) {
const observer = new IntersectionObserver((entries) => {
setIsOverScrollPosition(entries[0].boundingClientRect.y < 0);
});
const flagElement = document.createElement("div");
flagElement.id = elementId;
flagElement.className = "scroll-flag";
flagElement.style.top = position;
const container = document.getElementById("__next"); // React div id
const oldFlagElement = document.getElementById(elementId);
if (!oldFlagElement) container?.appendChild(flagElement);
const elementToObserve = oldFlagElement || flagElement;
observer.observe(elementToObserve);
}
}, [elementId, position]);
return isOverScrollPosition;
};
export default useCheckScrollPosition;
and then you can use it like this:
const isOverScrollPosition = useCheckScrollPosition({
elementId: "sticky-header",
position: "10px",
});
isOverScrollPosition is a boolean that will be true if you scroll over position provided value (10px) and false if you scroll below it.
This approach will add a flag div in react root.
Reference: https://css-tricks.com/styling-based-on-scroll-position/

Categories