Giter Site home page Giter Site logo

use-event-listener's Introduction

@use-it/event-listener

A custom React Hook that provides a declarative useEventListener.

npm version All Contributors

This hook was inspired by Dan Abramov's blog post "Making setInterval Declarative with React Hooks".

I needed a way to simplify the plumbing around adding and removing an event listener in a custom hook. That lead to a chain of tweets between Dan and myself.

Installation

$ npm i @use-it/event-listener

or

$ yarn add @use-it/event-listener

Usage

Here is a basic setup.

useEventListener(eventName, handler, element, options);

Parameters

Here are the parameters that you can use. (* = optional)

Parameter Description
eventName The event name (string). Here is a list of common events.
handler A function that will be called whenever eventName fires on element.
element* An optional element to listen on. Defaults to global (i.e., window).
options* An object { capture?: boolean, passive?: boolean, once?: boolean } to be passed to addEventListener. For advanced use cases. See MDN for details.

Return

This hook returns nothing.

Example

Let's look at some sample code. Suppose you would like to track the mouse position. You could subscribe to mouse move events with like this.

const useMouseMove = () => {
  const [coords, setCoords] = useState([0, 0]);

  useEffect(() => {
    const handler = ({ clientX, clientY }) => {
      setCoords([clientX, clientY]);
    };
    window.addEventListener('mousemove', handler);
    return () => {
      window.removeEventListener('mousemove', handler);
    };
  }, []);

  return coords;
};

Here we're using useEffect to roll our own handler add/remove event listener.

useEventListener abstracts this away. You only need to care about the event name and the handler function.

const useMouseMove = () => {
  const [coords, setCoords] = useState([0, 0]);

  useEventListener('mousemove', ({ clientX, clientY }) => {
    setCoords([clientX, clientY]);
  });

  return coords;
};

Live demo

You can view/edit the sample code above on CodeSandbox.

Edit demo app on CodeSandbox

License

MIT Licensed

Contributors

Thanks goes to these wonderful people (emoji key):


Donavon West

๐Ÿš‡ โš ๏ธ ๐Ÿ’ก ๐Ÿค” ๐Ÿšง ๐Ÿ‘€ ๐Ÿ”ง ๐Ÿ’ป

Kevin Kipp

๐Ÿ’ป

Justin Hall

๐Ÿ’ป ๐Ÿ“–

Jeow Li Huan

๐Ÿ‘€

Norman Rzepka

๐Ÿค”

Beer van der Drift

โš ๏ธ ๐Ÿ’ป

clingsoft

๐Ÿ’ป

This project follows the all-contributors specification. Contributions of any kind welcome!

use-event-listener's People

Contributors

allcontributors[bot] avatar bvanderdrift avatar dependabot[bot] avatar donavon avatar pruge avatar srmagura avatar third774 avatar valin4tor avatar wkovacs64 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

use-event-listener's Issues

Discuss design decision of underlying implementation

Currently this tiny useful hook is implemented like this:

import { useRef, useEffect } from 'react';

const useEventListener = (eventName, handler, element = global) => {
  const savedHandler = useRef();

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(
    () => {
      const isSupported = element && element.addEventListener;
      if (!isSupported) return;

      const eventListener = event => savedHandler.current(event);
      element.addEventListener(eventName, eventListener);
      return () => {
        element.removeEventListener(eventName, eventListener);
      };
    },
    [eventName, element]
  );
};

export default useEventListener;

Is useRef used for storing handler with the idea that handler may change more frequently than eventName or element and therefore to avoid removing and re-adding the event listener more frequently?

As I guess, if the hook were implemented like this:

import { useEffect } from 'react';

const useEventListener = (eventName, handler, element = global) => {
  useEffect(
    () => {
      const isSupported = element && element.addEventListener;
      if (!isSupported) return;

      element.addEventListener(eventName, handler);
      return () => {
        element.removeEventListener(eventName, handler);
      };
    },
    [eventName, element, handler]
  );
};

export default useEventListener;

Then the following client code:

  ...
  useEventListener('mousemove', ({ clientX, clientY }) => {
    setCoords([clientX, clientY]);
  });
  ...

Will cause the event listener to be removed (useEffect return function removing the listener called) and re-added on each render, even if eventName and element do not change.

Whereas, with your implementation and the above client code, the event listener is only removed when either element or eventName change, but not when the given handler changes on every render...
Meaning that whenever the component using this hook will re-render, only the useEffect hook's callback updating the savedHandler.current ref will be executed, and the useEffect hook's callback adding/removing the event listener will not execute.

This also means that using useCallback from the client code's side won't add much benefit:

  ...
  // handler never changes, only if setCoords changes...
  const handler = useCallback(
    ({ clientX, clientY }) => {
      setCoords({ x: clientX, y: clientY });
    },
    [setCoords]
  );

  // Passing a memoized handler callback won't add much benefit,
  // it will just avoid executing the `savedHandler.current = handler;` line of the `useEffect` hook
  // within `useEventListener`...
  useEventListener('mousemove', handler);
  ...

Are my assumptions and considerations right?

Thanks!

How to add listener on DOM element ?

Hello, I need to add a scroll listener on my component wrapper div. I've tried to create a ref (using useRef)

`const wrapperRef = useRef();

// Add event listener using our hook
useEventListener(
"scroll",
() => { ... },
wrapperRef.current
);`

But it doesn't work, because wrapperRef.current is undefined when I called useEventListener. How can I do please ?

Removing event listener

Thanks for the awesome hook!

Apologies if this is obvious but how would you remove the event listener? Let's say i'm attaching it based on the value of another variable..

if (editState === 'haschanges) {
  useEventListenter('beforeunload', handler);
} else {
  // remove the listener
}

Hook doesn't allow `element` to be null

I'd like to request that element should be allowed to be null, which makes sure initially there is a not a global event listener, there is no TS problems and the hooks are top-level.

Currently the element parameter typings are defined as

element?: HTMLElement

This causes problems in combination with useRef.

The following example:

const someRef = useRef();
useEventListener("click", onClickHandler, someRef.current);

works but will be listening to the click event on the global parameter initially (since current will be undefined in the first call). For example in the browser between useEffect and useLayoutEffect there will be time that the onClickHandler will be triggered when user clicks anywhere on the page.

A solution would be the have initial value of someRef.current be null but this throws a typescript error.

const someRef = useRef(null);
useEventListener("click", onClickHandler, someRef.current); //Type 'null' is not assignable to type 'HTMLElement | undefined'

One could circumvent this by putting the useEventListener in an if statement:

const someRef = useRef(null);
if(someRef.current !== null){
    useEventListener("click", onClickHandler, someRef.current);
}

But this creates the problem that useEventListener is not a top-level hook anymore which is required.

Therefore I'd suggest element is allowed to be null.

See #17 for the PR with my suggested solution.

[Question] Please show example how create another hook based on that

as example, i wanna create useResize hook, correct me please if im doing it wrong

const getSize = () => ({
  width: window.innerWidth,
  height: window.innerHeight,
});

export default function useResize(onResize = () => {}) {
  function handleResize() {
    if (typeof onResize === 'function') onResize(getSize());
  }

  // We need get window size on first call
  useEffect(() => {
    handleResize();
  }, []);

  // Then we are subscribe to window `resize`
  useEventListener('resize', handleResize);
}

Saving handler in useEffect()

The current implementation contains this:

useEffect(() => {
    savedHandler.current = handler;
}, [handler]);

Is there any reason this has to be in a useEffect()? I would just do:

savedHandler.current = handler;

I would think that would be quicker to just store handler every time than having useEffect() check to see if it's changed.

Or am I missing something?

Do not work, If we pass element as ref

If we pass element ref as below

const counterEl = useRef(null);

useEventListener('click', () => alert('Ref click'), counterEl);

  return (
    <div className="App">
      Count: <span ref={counterEl}>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );

But, It will work If we pass elementRef.current as below
useEventListener('click', () => alert('Ref click'), counterEl.current);

Will suggest a change, and If you allow will create a PR with the following change

    const targetIsRef = element.hasOwnProperty('current');
    const currentTarget = targetIsRef ? element.current : element;
    const isSupported = currentTarget && currentTarget.addEventListener;

Only start listener after some condition?

I am not sure if this is a great use case but sometimes we have code like this

useEffect(() => {
    if(mouseDown) {
        window.addEventListener('mouseup',...) // make sure to capture the mouseup even if it is outside of the target div
    }
    return () => window.removeEventListener('mouseup',...)
},[mouseDown])

<div onMouseDown={() => setMouseDown(true)}/>

Do you thing it's worth it to have a conditional use-event-listener for a case such as this? Or do you think code like this is maybe an antipattern

Feature request: have more specific parameter typings

When using element.useEventListener where element: HTMLElement there are two overloads (source):

    addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLAnchorElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

This enables the type checker in an IDE to defer the type of the event argument that is passed into the handler.

For example

buttonElement.addEventListener("click", (event) => doSomething());

will have IDEs recognize event as type MouseEvent.

I would suggest adding the overloads to the types of this package as well to enable that IDE feature.

Republish the package with updated peerDependencies 'cause now we have a problem for React 17

Hi! If you look at the current package.json of this package published on NPM: https://unpkg.com/@use-it/[email protected]/package.json

{
  ...
  "peerDependencies": {
    "react": "^16.8.0"
  },
  ...
}

You will see that it only peer-depends on React ^16, whereas as React 17 is now released, it should be:

{
  ...
  "peerDependencies": {
    "react": "^16.8.0 || ^17.0.0"
  },
  ...
}

With the current package.json, NPM returns the following error when I run npm update:

$ npm update
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! 
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR!   react@"^17.0.1" from the root project
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.8.0" from @use-it/[email protected]
npm ERR! node_modules/@use-it/event-listener
npm ERR!   @use-it/event-listener@"^0.1.6" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR! 
npm ERR! See /Users/antony/.npm/eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/antony/.npm/_logs/2021-02-26T09_51_18_073Z-debug.log

Could you publish the updated package.json file, please? Thank you!

Is useEffect necessary to update the ref? (genuine question)

While reading the code, I realized the ref handler was updated differently from what I am used to. So I am genuinely wondering if there's some kind of best practice or performance implication I am missing.

Here's how the ref is currently updated:

const useEventListener = (eventName, handler, element = global) => {
  const savedHandler = useRef();

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  // The rest of the code goes here...
};

And here's how I usually update it:

const useEventListener = (eventName, handler, element = global) => {
  const savedHandler = useRef();
  savedHandler.current = handler;

  // The rest of the code goes here...
};

From my understanding, references hold a mutable value that can be changed without triggering rerenders. They're really just "values floating around" so changing it this way has no side effects.

Are these two approaches equivalent? Thanks in advance! ๐Ÿ™

TypeScript definitions in repo do not match with latest version on npm

Expected behavior

When installing the latest version of @use-it/event-listener from npm, the TypeScript definitions should be the same as in the latest repo version (7fd6e56#diff-88bec6beae84369c0376b56b3bb88fe1).

Actual behavior

When installing @use-it/[email protected] from npm, the package contains older TypeScript definitions (8494f30#diff-88bec6beae84369c0376b56b3bb88fe1). On top of that, it seems to me that they are incorrect. Since the hook uses a native host functionality to subscribe to DOM events, the type for a received should not be SyntheticEvent.

Resolution

Is it possible to release a newer version to npm with the updated TypeScript definitions?

How to add listener for particular group of DOM Elements?

I would like to add some global click event to some dom elements (e.g based on className)

i tried this below code

const Mouse = () => {
  const [element, setElement] = useState(null);
  const [x, y] = useMouseMove(element);
  return (
    <React.Fragment>
      <div ref={el => setElement(document.getElementsByClassName(".globalMouseMove"))}>
      <h1 className="globalMouseMove">
      The mouse position is ({x}, {y})
    </h1>
        <h1 id="k" className="globalMouseMove">
        The mouse position is ({x}, {y})
      </h1>
      </div>
    </React.Fragment>
  );
};

Support for event listener options

Can we have something like this?

useEventListener('click', () => {
  console.log('The window is clicked and cannot be clicked anymore');
}, { once: true });

How to prevent event propagation / bubbling?

When I use the hook to focus a MUI text field, it not only focuses the field but also types the character I was listening for into the field.

I'm thinking I need to stop event propagation but I'm not catching on to how to do that (feeling a bit dim about it...).

Simple enough code. How to stop the event?

useEventListener('keydown', ({ key }) => {
  if (key === '/') {
    document.getElementById('dataGridSearch').focus();
  }
});

eslint conditional issue

eslint on my codesandbox is giving me this issue:

A custom React Hook that provides a declarative useEventListener. React Hook "useEventListener" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? (react-hooks/rules-of-hooks)eslint

Im using a function for left, right, and escape keypress for a carousel modal like so:

const ESCAPE_KEYS = ["27", "Escape"];
  const ARROWLEFT_KEYS = ["37", "ArrowLeft"];
  const ARROWRIGHT_KEYS = ["39", "ArrowRight"];

 function keypressHandler ({ key }) {
    // e = e || window.event;
    if (ARROWLEFT_KEYS.includes(String(key))) {
      // setCurrent(current === length - 1 ? 0 : current + 1); //left <- show Prev image
      prevSlide();
      console.log("Left key pressed!");
    } else if (ARROWRIGHT_KEYS.includes(String(key))) {
      // right -> show next image
      // setCurrent(current + 1);
      nextSlide();
      console.log("Right key pressed!");
    }

    if (ESCAPE_KEYS.includes(String(key))) {
      onClose();
      console.log("Escape key pressed!");
    }
    // console.log("event: ", e);
  };

But the code still seems to work though so I guess I'm just gonna have to ignore this issue.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.