Skip to content

Commit 2c82483

Browse files
committed
[ADD] useEventListener hook
1 parent 94af9f3 commit 2c82483

15 files changed

Lines changed: 257 additions & 21 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useRef } from "react";
2+
import { useEventListener } from "../../../../../../packages/react-tools/src/hooks";
3+
4+
/**
5+
The component has:
6+
- A _buttonRef_ ref variable linked to an button node element with text click to log.
7+
- A _useEventListener_ invocation with these options: _type_="click", _listener_=(e:Event)=>console.log(e). So the eventListener is attached to window and the invocation result is used to initialize a variable remove that remove the eventListener.
8+
- A _useEventListener_ invocation like that above but attached at button node by _buttonRef_ with _listenerOpts_ capture=_true_ and that stops event propagation.
9+
- A _button_ with text Remove that, if clicked, invokes the removeEventListener.
10+
11+
At every click on Demo area the eventListener attached on object window logs in console the event, while the eventListener attached on button logs when button is clicked. When button remove is clicked it removes the eventListener on window object. The button eventListener is removed during component unmount.
12+
*/
13+
const UseEventListener = () => {
14+
const buttonRef = useRef<HTMLButtonElement>(null);
15+
const remove = useEventListener({ type: "click", listener: (e: Event) => console.log(e) });
16+
useEventListener({
17+
type: "click", listener: (e: Event) => {
18+
console.log(e);
19+
e.stopPropagation();
20+
}, element: buttonRef, listenerOpts: {capture: true} });
21+
22+
return (<>
23+
<button onClick={() => remove()}>Remove</button>
24+
<button ref={buttonRef} style={{marginLeft: 10}}>click to log</button>
25+
</>);
26+
}
27+
28+
UseEventListener.displayName = "UseEventListener";
29+
30+
export { UseEventListener };
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useCallback, useEffect } from "react";
2+
3+
/**
4+
DEMO
5+
*/
6+
const UseLocalStorage = () => {
7+
const func = useCallback(() => {
8+
window.localStorage.setItem(Math.random() > 0.5 ? "demo" : "prv", "2");
9+
window.dispatchEvent(new Event("storage"));
10+
}, []);
11+
12+
const clear = useCallback(() => window.localStorage.clear(), []);
13+
14+
useEffect(() => {
15+
const listener = (evt: Event) => console.log(evt);
16+
window.addEventListener("storage", listener);
17+
18+
return () => window.removeEventListener("storage", listener);
19+
},[])
20+
return (<>
21+
<button onClick={func}>Prova</button>
22+
<button onClick={clear}>clear</button>
23+
</>);
24+
}
25+
26+
UseLocalStorage.displayName = "UseLocalStorage";
27+
28+
export { UseLocalStorage };

apps/react-tools-demo/src/constants/components.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const COMPONENTS = [
77
"useReducerGetReset",
88
"useReducerHistory",
99
"useReducerHistoryGetter",
10+
"useLocalStorage",
1011
"useMemoizedFunction",
1112
"useMemoCompare",
1213
"useMemoDeepCompare",
@@ -16,7 +17,8 @@ export const COMPONENTS = [
1617
"useEffectDeepCompare",
1718
"useLayoutEffectCompare",
1819
"useLayoutEffectDeepCompare",
19-
"usePubSubModel"
20+
"usePubSubModel",
21+
"useEventListener"
2022
],
2123
[
2224
"isShallowEqual",

apps/react-tools-demo/src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import ReactDOM from 'react-dom/client'
22
import App from './App.tsx'
33
import './index.css'
4+
import { StrictMode } from 'react'
45

56
ReactDOM.createRoot(document.getElementById('root')!).render(
6-
<App />
7+
<StrictMode>
8+
<App />
9+
</StrictMode>
710
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# useEventListener
2+
Hook to simplify add and remove EventListener use. It's persist during rerendering and automatically remove eventlistener on unMount component lifecycle.
3+
4+
## Usage
5+
6+
```tsx
7+
const UseEventListener = () => {
8+
const buttonRef = useRef<HTMLButtonElement>(null);
9+
const remove = useEventListener({ type: "click", listener: (e: Event) => console.log(e) });
10+
useEventListener({
11+
type: "click", listener: (e: Event) => {
12+
console.log(e);
13+
e.stopPropagation();
14+
}, element: buttonRef, listenerOpts: {capture: true} });
15+
16+
return (<>
17+
<button onClick={() => remove()}>Remove</button>
18+
<button ref={buttonRef} style={{marginLeft: 10}}>click to log</button>
19+
</>);
20+
}
21+
22+
UseEventListener.displayName = "UseEventListener";
23+
24+
export { UseEventListener };
25+
```
26+
27+
> The component has:
28+
> - A _buttonRef_ ref variable linked to an button node element.
29+
> - A _useEventListener_ invocation with these options: _type_="click", _listener_=(e:Event)=>console.log(e). So the eventListener is attached to window and the invocation result is used to initialize a variable remove that remove the eventListener.
30+
> - A _useEventListener_ invocation like that above but attached at button node by _buttonRef_ with _listenerOpts_ capture=_true_ and that stops event propagation.
31+
> - A button that, if clicked, invokes the removeEventListener
32+
> At every click on Demo area the eventListener attached on object window logs in console the event, while the eventListener attached on button logs when button is clicked. When button remove is clicked it removes the eventListener on window object. The button eventListener is removed during component unmount.
33+
34+
35+
## API
36+
37+
```tsx
38+
useEventListener ({ type, listener, element = window, listenerOpts }: { type: string, listener: (evt: Event | CustomEvent) => void, element?: RefObject<HTMLElement> | Window, listenerOpts?: boolean | AddEventListenerOptions})
39+
```
40+
41+
> ### Params
42+
>
43+
> - __options__: _Object_
44+
> - __options.type__: _string_
45+
event type
46+
> - __options.listener__: _(evt: Event | CustomEvent) => void_
47+
listener to be executed on specified event
48+
> - __options.element=window?__: _RefObject<HTMLElement> | Window_
49+
element on which attaching eventListener
50+
> - __options.listenerOpts?__: _boolean | AddEventListenerOptions_
51+
options for listener
52+
>
53+
54+
> ### Returns
55+
>
56+
> __remove__: used to manually remove the eventListener
57+
> - _()=>void_
58+
>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#
2+
3+
4+
## Usage
5+
6+
```tsx
7+
const UseLocalStorage = () => {
8+
const func = useCallback(() => {
9+
window.localStorage.setItem(Math.random() > 0.5 ? "demo" : "prv", "2");
10+
window.dispatchEvent(new Event("storage"));
11+
}, []);
12+
13+
const clear = useCallback(() => window.localStorage.clear(), []);
14+
15+
useEffect(() => {
16+
const listener = (evt: Event) => console.log(evt);
17+
window.addEventListener("storage", listener);
18+
19+
return () => window.removeEventListener("storage", listener);
20+
},[])
21+
return (<>
22+
<button onClick={func}>Prova</button>
23+
<button onClick={clear}>clear</button>
24+
</>);
25+
}
26+
27+
UseLocalStorage.displayName = "UseLocalStorage";
28+
29+
export { UseLocalStorage };
30+
```
31+
32+
> DEMO
33+
34+
35+
## API
36+
37+
```tsx
38+
useLocalStorage = <T>(key: string, initialState?: T | (()=>T))
39+
```
40+
41+
> ### Params
42+
>
43+
>
44+
>
45+
46+
> ### Returns
47+
>
48+
>
49+
>
50+
>

apps/react-tools-demo/src/markdown/useMemoizedFunction.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# useMemoizedFunction
2-
Hook to store a function that will never change while keeping its dependencies always up to date. Can be used instead of _useCallback_.
2+
Hook to store a function that will never change while keeping its dependencies always up to date. Can be used instead of _useCallback_, without esplicity dependencies array.
33

44
## Usage
55

@@ -45,7 +45,7 @@ export {UseMemoizedFunction}
4545
## API
4646

4747
```tsx
48-
useMemoizedFunction <T extends Function>(fn: T)
48+
useMemoizedFunction <T extends (...args: any[]) => any>(fn: T)
4949
```
5050

5151
> ### Params

apps/react-tools-demo/src/router/Router.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import useEffectCompareMD from '../markdown/useEffectCompare.md?url';
1919
import useEffectDeepCompareMD from '../markdown/useEffectDeepCompare.md?url';
2020
import useLayoutEffectCompareMD from '../markdown/useLayoutEffectCompare.md?url';
2121
import useLayoutEffectDeepCompareMD from '../markdown/useLayoutEffectDeepCompare.md?url';
22+
import useEventListenerMD from '../markdown/useEventListener.md?url';
2223
import isShallowEqualMD from '../markdown/isShallowEqual.md?url';
2324
import isDeepEqualMD from '../markdown/isDeepEqual.md?url';
2425
import isMouseEventMD from '../markdown/isMouseEvent.md?url';
@@ -36,6 +37,8 @@ import { UseEffectDeepCompare } from '../components/hooks/useEffectDeepCompare/U
3637
import { UseStateGetReset } from '../components/hooks/useStateGetReset/UseStateGetReset';
3738
import { UsePubSubModel } from '../components/hooks/usePubSubModel/UsePubSubModel';
3839
import { UseMemoizedFunction } from '../components/hooks/useMemoizedFunction/UseMemoizedFunction';
40+
import { UseLocalStorage } from '../components/hooks/useLocalStorage/UseLocalStorage';
41+
import { UseEventListener } from '../components/hooks/useEventListener/UseEventListener';
3942

4043
function Router() {
4144
const router = createBrowserRouter([
@@ -92,6 +95,10 @@ function Router() {
9295
source={useReducerHistoryGetterMD}
9396
/>
9497
},
98+
{
99+
path: "/useLocalStorage",
100+
element: <UseLocalStorage/>
101+
},
95102
{
96103
path: "/useMemoizedFunction",
97104
element: <ComponentLayout
@@ -160,6 +167,13 @@ function Router() {
160167
markdown={usePubSubModelMD}
161168
/>
162169
},
170+
{
171+
path: "/useEventListener",
172+
element: <ComponentLayout
173+
component={<UseEventListener />}
174+
markdown={useEventListenerMD}
175+
/>
176+
},
163177
{
164178
path: "/isShallowEqual",
165179
element: <MarkdownLayout

packages/react-tools/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
- [x] useReducerGetReset
88
- [x] useReducerHistory
99
- [x] useReducerHistoryGetter
10-
- [ ] useLocalStorage
10+
- [-] useLocalStorage
1111
- [ ] useSessionStorage
1212
- [ ] useMap
1313
- [ ] useSet
1414
- [ ] useArray
1515
- __EFFECTS__
1616
- [x] useCallbackCompare
1717
- [x] useCallbackDeepCompare
18-
- [-] useMemoizedFunction (missing example)
18+
- [x] useMemoizedFunction
1919
- [x] useMemoCompare
2020
- [x] useMemoDeepCompare
2121
- [x] useEffectCompare
@@ -29,7 +29,7 @@
2929
- __EVENTS__
3030
- [ ] useDebounce
3131
- [ ] useThrottle
32-
- [ ] useEventListener
32+
- [x] useEventListener
3333
- [ ] useActiveElement
3434
- [ ] useSize
3535
- [ ] useHover

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export { useEffectCompare } from "./useEffectCompare";
1313
export { useEffectDeepCompare } from "./useEffectDeepCompare";
1414
export { useLayoutEffectCompare } from "./useLayoutEffectCompare";
1515
export { useLayoutEffectDeepCompare } from "./useLayoutEffectDeepCompare";
16-
export { usePubSubModel } from './usePubSubModel';
16+
export { usePubSubModel } from './usePubSubModel';
17+
export { useEventListener } from './useEventListener';

0 commit comments

Comments
 (0)