@aweebit/react-essentials 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,8 @@ import type { ArgumentFallback } from '../utils.js';
6
6
  * as an argument to `useContext` or `use` (the hook produced with
7
7
  * {@linkcode createSafeContext} should be used instead)
8
8
  *
9
- * @see {@linkcode createSafeContext}
9
+ * @see
10
+ * {@linkcode createSafeContext}
10
11
  */
11
12
  export type RestrictedContext<T> = Context<T> extends Provider<T> ? {
12
13
  Provider: Provider<T>;
@@ -16,7 +17,11 @@ export type RestrictedContext<T> = Context<T> extends Provider<T> ? {
16
17
  displayName: string;
17
18
  };
18
19
  /**
19
- * @see {@linkcode createSafeContext}
20
+ * The return type of {@linkcode createSafeContext}
21
+ *
22
+ * @see
23
+ * {@linkcode createSafeContext},
24
+ * {@linkcode RestrictedContext}
20
25
  */
21
26
  export type SafeContext<DisplayName extends string, T> = {
22
27
  [K in `${DisplayName}Context`]: RestrictedContext<T>;
@@ -28,25 +33,59 @@ export type SafeContext<DisplayName extends string, T> = {
28
33
  * type and a hook that returns the current context value if one was provided,
29
34
  * or throws an error otherwise
30
35
  *
36
+ * The advantages over vanilla `createContext` are that no default value has to
37
+ * be provided, and that a meaningful context name is displayed in dev tools
38
+ * instead of generic `Context.Provider`.
39
+ *
31
40
  * @example
32
41
  * ```tsx
33
- * const { ItemsContext, useItems } = createSafeContext<string[]>()('Items');
42
+ * enum Direction {
43
+ * Up,
44
+ * Down,
45
+ * Left,
46
+ * Right,
47
+ * }
48
+ *
49
+ * // Before
50
+ * const DirectionContext = createContext<Direction | undefined>(undefined);
51
+ * DirectionContext.displayName = 'DirectionContext';
52
+ *
53
+ * const useDirection = () => {
54
+ * const direction = useContext(DirectionContext);
55
+ * if (direction === undefined) {
56
+ * // Called outside of a <DirectionContext.Provider> boundary!
57
+ * // Or maybe undefined was explicitly provided as the context value
58
+ * // (ideally that shouldn't be allowed, but it is because we had to include
59
+ * // undefined in the context type so as to provide a meaningful default)
60
+ * throw new Error('No DirectionContext value was provided');
61
+ * }
62
+ * // Thanks to the undefined check, the type is now narrowed down to Direction
63
+ * return direction;
64
+ * };
65
+ *
66
+ * // After
67
+ * const { DirectionContext, useDirection } =
68
+ * createSafeContext<Direction>()('Direction'); // That's it :)
34
69
  *
35
70
  * const Parent = () => (
36
- * <ItemsContext value={['compass', 'newspaper', 'banana']}>
71
+ * // Providing undefined as the value is not allowed 👍
72
+ * <Direction.Provider value={Direction.Up}>
37
73
  * <Child />
38
- * </ItemsContext>
74
+ * </Direction.Provider>
39
75
  * );
40
76
  *
41
- * const Child = () => useItems().join(', ');
77
+ * const Child = () => `Current direction: ${Direction[useDirection()]}`;
42
78
  * ```
43
79
  *
44
80
  * @returns
45
81
  * A function that accepts a single string argument `displayName` (e.g.
46
- * `"Items"`) and returns an object with the following properties:
47
- * - ``` `${displayName}Context` ``` (e.g. `ItemsContext`): the context
48
- * - ``` `use${displayName}` ``` (e.g. `useItems`): a hook that returns the
82
+ * `"Direction"`) and returns an object with the following properties:
83
+ * - ``` `${displayName}Context` ``` (e.g. `DirectionContext`): the context
84
+ * - ``` `use${displayName}` ``` (e.g. `useDirection`): a hook that returns the
49
85
  * current context value if one was provided, or throws an error otherwise
86
+ *
87
+ * @see
88
+ * {@linkcode SafeContext}
50
89
  */
51
90
  export declare function createSafeContext<T = never>(): <DisplayName extends string>(displayName: [T] extends [never] ? never : ArgumentFallback<DisplayName, never, string>) => SafeContext<DisplayName, T>;
52
91
  //# sourceMappingURL=createSafeContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createSafeContext.d.ts","sourceRoot":"","sources":["../../src/misc/createSafeContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAA6B,MAAM,OAAO,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD;;;;;;;GAOG;AAIH,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAC7B,OAAO,CAAC,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,GAC1B;IAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,GAC5D;IAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,WAAW,SAAS,MAAM,EAAE,CAAC,IAAI;KACtD,CAAC,IAAI,GAAG,WAAW,SAAS,GAAG,iBAAiB,CAAC,CAAC,CAAC;CACrD,GAAG;KACD,CAAC,IAAI,MAAM,WAAW,EAAE,GAAG,MAAM,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,GAAG,KAAK,MACjC,WAAW,SAAS,MAAM,EAChC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAC5B,KAAK,GACL,gBAAgB,CAAC,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,KAC/C,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAsB/B"}
1
+ {"version":3,"file":"createSafeContext.d.ts","sourceRoot":"","sources":["../../src/misc/createSafeContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAA6B,MAAM,OAAO,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD;;;;;;;;GAQG;AAIH,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAC7B,OAAO,CAAC,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,GAC1B;IAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,GAC5D;IAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,CAAC,WAAW,SAAS,MAAM,EAAE,CAAC,IAAI;KACtD,CAAC,IAAI,GAAG,WAAW,SAAS,GAAG,iBAAiB,CAAC,CAAC,CAAC;CACrD,GAAG;KACD,CAAC,IAAI,MAAM,WAAW,EAAE,GAAG,MAAM,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,GAAG,KAAK,MACjC,WAAW,SAAS,MAAM,EAChC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAC5B,KAAK,GACL,gBAAgB,CAAC,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,KAC/C,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAsB/B"}
@@ -5,25 +5,59 @@ const moValueSymbol = Symbol('noValue');
5
5
  * type and a hook that returns the current context value if one was provided,
6
6
  * or throws an error otherwise
7
7
  *
8
+ * The advantages over vanilla `createContext` are that no default value has to
9
+ * be provided, and that a meaningful context name is displayed in dev tools
10
+ * instead of generic `Context.Provider`.
11
+ *
8
12
  * @example
9
13
  * ```tsx
10
- * const { ItemsContext, useItems } = createSafeContext<string[]>()('Items');
14
+ * enum Direction {
15
+ * Up,
16
+ * Down,
17
+ * Left,
18
+ * Right,
19
+ * }
20
+ *
21
+ * // Before
22
+ * const DirectionContext = createContext<Direction | undefined>(undefined);
23
+ * DirectionContext.displayName = 'DirectionContext';
24
+ *
25
+ * const useDirection = () => {
26
+ * const direction = useContext(DirectionContext);
27
+ * if (direction === undefined) {
28
+ * // Called outside of a <DirectionContext.Provider> boundary!
29
+ * // Or maybe undefined was explicitly provided as the context value
30
+ * // (ideally that shouldn't be allowed, but it is because we had to include
31
+ * // undefined in the context type so as to provide a meaningful default)
32
+ * throw new Error('No DirectionContext value was provided');
33
+ * }
34
+ * // Thanks to the undefined check, the type is now narrowed down to Direction
35
+ * return direction;
36
+ * };
37
+ *
38
+ * // After
39
+ * const { DirectionContext, useDirection } =
40
+ * createSafeContext<Direction>()('Direction'); // That's it :)
11
41
  *
12
42
  * const Parent = () => (
13
- * <ItemsContext value={['compass', 'newspaper', 'banana']}>
43
+ * // Providing undefined as the value is not allowed 👍
44
+ * <Direction.Provider value={Direction.Up}>
14
45
  * <Child />
15
- * </ItemsContext>
46
+ * </Direction.Provider>
16
47
  * );
17
48
  *
18
- * const Child = () => useItems().join(', ');
49
+ * const Child = () => `Current direction: ${Direction[useDirection()]}`;
19
50
  * ```
20
51
  *
21
52
  * @returns
22
53
  * A function that accepts a single string argument `displayName` (e.g.
23
- * `"Items"`) and returns an object with the following properties:
24
- * - ``` `${displayName}Context` ``` (e.g. `ItemsContext`): the context
25
- * - ``` `use${displayName}` ``` (e.g. `useItems`): a hook that returns the
54
+ * `"Direction"`) and returns an object with the following properties:
55
+ * - ``` `${displayName}Context` ``` (e.g. `DirectionContext`): the context
56
+ * - ``` `use${displayName}` ``` (e.g. `useDirection`): a hook that returns the
26
57
  * current context value if one was provided, or throws an error otherwise
58
+ *
59
+ * @see
60
+ * {@linkcode SafeContext}
27
61
  */
28
62
  export function createSafeContext() {
29
63
  return (displayName) => {
@@ -1 +1 @@
1
- {"version":3,"file":"createSafeContext.js","sourceRoot":"","sources":["../../src/misc/createSafeContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAG/E,MAAM,aAAa,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;AA2BxC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,CACL,WAEgD,EACnB,EAAE;QAC/B,MAAM,WAAW,GAAG,GAAG,WAA0B,SAAkB,CAAC;QACpE,MAAM,QAAQ,GAAG,MAAM,WAA0B,EAAW,CAAC;QAE7D,MAAM,OAAO,GAAG,aAAa,CAA2B,aAAa,CAAC,CAAC;QACvE,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;QAElC,OAAO;YACL,CAAC,WAAW,CAAC,EAAE,OAA+B;YAC9C,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE;gBACf,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;gBAClC,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;oBAC5B,MAAM,IAAI,KAAK,CAAC,MAAM,WAAW,qBAAqB,CAAC,CAAC;gBAC1D,CAAC;gBACD,OAAO,KAAK,CAAC;YACf,CAAC;SAKF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"createSafeContext.js","sourceRoot":"","sources":["../../src/misc/createSafeContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAG/E,MAAM,aAAa,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;AAgCxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,CACL,WAEgD,EACnB,EAAE;QAC/B,MAAM,WAAW,GAAG,GAAG,WAA0B,SAAkB,CAAC;QACpE,MAAM,QAAQ,GAAG,MAAM,WAA0B,EAAW,CAAC;QAE7D,MAAM,OAAO,GAAG,aAAa,CAA2B,aAAa,CAAC,CAAC;QACvE,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;QAElC,OAAO;YACL,CAAC,WAAW,CAAC,EAAE,OAA+B;YAC9C,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE;gBACf,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;gBAClC,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;oBAC5B,MAAM,IAAI,KAAK,CAAC,MAAM,WAAW,qBAAqB,CAAC,CAAC;gBAC1D,CAAC;gBACD,OAAO,KAAK,CAAC;YACf,CAAC;SAKF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aweebit/react-essentials",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "repository": "github:aweebit/react-essentials",
6
6
  "main": "dist/index.js",
@@ -5,101 +5,113 @@
5
5
  * @copyright 2020 Julien CARON
6
6
  */
7
7
 
8
- import { useEffect, useMemo, useRef } from 'react';
8
+ import { useEffect, useMemo, useRef, type RefObject } from 'react';
9
9
 
10
10
  /**
11
- * Adds `handler` as a listener for the event `eventName` of `element` with the
12
- * provided `options` applied
13
- *
14
- * If `element` is `undefined`, `window` is used instead.
15
- *
16
- * If `element` is `null`, no event listener is added.
17
- *
18
- * @example
19
- * ```tsx
20
- * useEventListener('resize', () => {
21
- * console.log(window.innerWidth, window.innerHeight);
22
- * });
23
- *
24
- * useEventListener(
25
- * 'visibilitychange',
26
- * () => console.log(document.visibilityState),
27
- * document
28
- * );
29
- *
30
- * const buttonRef = useRef<HTMLButtonElement>(null);
31
- * useEventListener("click", () => console.log("click"), buttonRef.current);
32
- * ```
11
+ * The type of {@linkcode useEventListener}
33
12
  *
34
- * @ignore
13
+ * @see
14
+ * {@linkcode useEventListener},
15
+ * {@linkcode UseEventListenerWithImplicitWindowTarget},
16
+ * {@linkcode UseEventListenerWithExplicitTarget},
17
+ * {@linkcode UseEventListenerWithAnyExplicitTarget}
35
18
  */
36
- export function useEventListener<
37
- K extends keyof HTMLElementEventMap,
38
- T extends HTMLElement,
19
+ export type UseEventListener = UseEventListenerWithImplicitWindowTarget &
20
+ UseEventListenerWithExplicitTarget<Window, WindowEventMap> &
21
+ UseEventListenerWithExplicitTarget<Document, DocumentEventMap> &
22
+ UseEventListenerWithExplicitTarget<HTMLElement, HTMLElementEventMap> &
23
+ UseEventListenerWithExplicitTarget<SVGElement, SVGElementEventMap> &
24
+ UseEventListenerWithExplicitTarget<MathMLElement, MathMLElementEventMap> &
25
+ UseEventListenerWithAnyExplicitTarget;
26
+
27
+ /**
28
+ * @see
29
+ * {@linkcode useEventListener},
30
+ * {@linkcode UseEventListenerWithImplicitWindowTargetArgs} */
31
+ export type UseEventListenerWithImplicitWindowTarget = <
32
+ K extends keyof WindowEventMap,
39
33
  >(
40
- eventName: K,
41
- handler: (this: NoInfer<T>, event: HTMLElementEventMap[K]) => void,
42
- element: T | null,
43
- options?: boolean | AddEventListenerOptions,
44
- ): void;
34
+ ...args: UseEventListenerWithImplicitWindowTargetArgs<K>
35
+ ) => void;
45
36
 
46
37
  /**
47
- * @see {@linkcode useEventListener}
48
- * @ignore
38
+ * @see
39
+ * {@linkcode useEventListener},
40
+ * {@linkcode UseEventListenerWithExplicitTargetArgs}
49
41
  */
50
- export function useEventListener<
51
- K extends keyof SVGElementEventMap,
52
- T extends SVGElement,
53
- >(
54
- eventName: K,
55
- handler: (this: NoInfer<T>, event: SVGElementEventMap[K]) => void,
56
- element: T | null,
57
- options?: boolean | AddEventListenerOptions,
58
- ): void;
42
+ export type UseEventListenerWithExplicitTarget<
43
+ Target extends EventTarget,
44
+ EventMap = Record<string, Event>,
45
+ > = <T extends Target, K extends keyof EventMap>(
46
+ ...args: UseEventListenerWithExplicitTargetArgs<EventMap, T, K>
47
+ ) => void;
59
48
 
60
49
  /**
61
- * @see {@linkcode useEventListener}
62
- * @ignore
50
+ * @see
51
+ * {@linkcode useEventListener},
52
+ * {@linkcode UseEventListenerWithExplicitTarget}
63
53
  */
64
- export function useEventListener<
65
- K extends keyof MathMLElementEventMap,
66
- T extends MathMLElement,
67
- >(
68
- eventName: K,
69
- handler: (this: NoInfer<T>, event: MathMLElementEventMap[K]) => void,
70
- element: T | null,
71
- options?: boolean | AddEventListenerOptions,
72
- ): void;
54
+ export type UseEventListenerWithAnyExplicitTarget =
55
+ UseEventListenerWithExplicitTarget<EventTarget>;
73
56
 
74
57
  /**
75
- * @see {@linkcode useEventListener}
76
- * @ignore
58
+ * @see
59
+ * {@linkcode useEventListener},
60
+ * {@linkcode UseEventListenerWithExplicitTargetArgs}
77
61
  */
78
- export function useEventListener<K extends keyof DocumentEventMap>(
79
- eventName: K,
80
- handler: (this: Document, event: DocumentEventMap[K]) => void,
81
- element: Document,
82
- options?: boolean | AddEventListenerOptions,
83
- ): void;
62
+ export type UseEventListenerWithImplicitWindowTargetArgs<
63
+ K extends keyof WindowEventMap,
64
+ > =
65
+ UseEventListenerWithExplicitTargetArgs<WindowEventMap, Window, K> extends [
66
+ unknown,
67
+ ...infer Args,
68
+ ]
69
+ ? Args
70
+ : never;
84
71
 
85
72
  /**
86
- * @see {@linkcode useEventListener}
87
- * @ignore
73
+ * @see
74
+ * {@linkcode useEventListener}
88
75
  */
89
- export function useEventListener<K extends keyof WindowEventMap>(
76
+ export type UseEventListenerWithExplicitTargetArgs<
77
+ EventMap,
78
+ T extends EventTarget,
79
+ K extends keyof EventMap,
80
+ > = [
81
+ target: T | (RefObject<T> & { addEventListener?: never }) | null,
90
82
  eventName: K,
91
- handler: (this: Window, event: WindowEventMap[K]) => void,
92
- element?: Window,
93
- options?: boolean | AddEventListenerOptions,
94
- ): void;
83
+ handler: (this: NoInfer<T>, event: EventMap[K]) => void,
84
+ options?: AddEventListenerOptions | boolean | undefined,
85
+ ];
86
+
87
+ type UseEventListenerWithImplicitWindowTargetArgsAny =
88
+ UseEventListenerWithImplicitWindowTargetArgs<keyof WindowEventMap>;
89
+
90
+ type UseEventListenerWithExplicitTargetArgsAny =
91
+ UseEventListenerWithExplicitTargetArgs<
92
+ Record<string, Event>,
93
+ EventTarget,
94
+ string
95
+ >;
95
96
 
96
97
  /**
97
- * Adds `handler` as a listener for the event `eventName` of `element` with the
98
+ * Adds `handler` as a listener for the event `eventName` of `target` with the
98
99
  * provided `options` applied
99
100
  *
100
- * If `element` is `undefined`, `window` is used instead.
101
+ * The following call signatures are available:
102
+ *
103
+ * ```ts
104
+ * function useEventListener(eventName, handler, options?): void;
105
+ * function useEventListener(target, eventName, handler, options?): void;
106
+ * ```
107
+ *
108
+ * For the full definition of the hook's type, see {@linkcode UseEventListener}.
101
109
  *
102
- * If `element` is `null`, no event listener is added.
110
+ * If `target` is not provided, `window` is used instead.
111
+ *
112
+ * If `target` is `null`, no event listener is added. This is useful when
113
+ * working with DOM element refs, or when the event listener needs to be removed
114
+ * temporarily.
103
115
  *
104
116
  * @example
105
117
  * ```tsx
@@ -107,29 +119,35 @@ export function useEventListener<K extends keyof WindowEventMap>(
107
119
  * console.log(window.innerWidth, window.innerHeight);
108
120
  * });
109
121
  *
110
- * useEventListener(
111
- * 'visibilitychange',
112
- * () => console.log(document.visibilityState),
113
- * document
114
- * );
122
+ * useEventListener(document, 'visibilitychange', () => {
123
+ * console.log(document.visibilityState);
124
+ * });
115
125
  *
116
126
  * const buttonRef = useRef<HTMLButtonElement>(null);
117
- * useEventListener("click", () => console.log("click"), buttonRef.current);
127
+ * useEventListener(buttonRef, 'click', () => console.log('click'));
118
128
  * ```
129
+ *
130
+ * @see
131
+ * {@linkcode UseEventListener}
119
132
  */
120
- export function useEventListener<T extends EventTarget>(
121
- eventName: string,
122
- handler: (this: NoInfer<T>, event: Event) => void,
123
- element?: T | null,
124
- options?: boolean | AddEventListenerOptions,
125
- ): void;
126
-
127
- export function useEventListener(
128
- eventName: string,
129
- handler: (this: EventTarget, event: Event) => void,
130
- element?: EventTarget | null,
131
- options?: boolean | AddEventListenerOptions,
133
+ export const useEventListener: UseEventListener = function useEventListener(
134
+ ...args:
135
+ | UseEventListenerWithImplicitWindowTargetArgsAny
136
+ | UseEventListenerWithExplicitTargetArgsAny
132
137
  ) {
138
+ const [target, eventName, handler, options]: [
139
+ target: EventTarget | RefObject<EventTarget> | null,
140
+ eventName: string,
141
+ handler: (this: never, event: Event) => void,
142
+ options?: AddEventListenerOptions | boolean | undefined,
143
+ ] =
144
+ typeof args[0] === 'string'
145
+ ? [window, ...(args as UseEventListenerWithImplicitWindowTargetArgsAny)]
146
+ : (args as UseEventListenerWithExplicitTargetArgsAny);
147
+
148
+ const unwrappedTarget =
149
+ target && !('addEventListener' in target) ? target.current : target;
150
+
133
151
  const handlerRef = useRef(handler);
134
152
  handlerRef.current = handler;
135
153
 
@@ -147,24 +165,19 @@ export function useEventListener(
147
165
  );
148
166
 
149
167
  useEffect(() => {
150
- if (element === null) {
168
+ if (unwrappedTarget === null) {
151
169
  // No element has been attached to the ref yet
152
170
  return;
153
171
  }
154
172
 
155
- // Define the listening target
156
- const targetElement = element ?? window;
157
-
158
- // Create event listener that calls handler function stored in ref
159
173
  const listener: typeof handler = function (event) {
160
174
  handlerRef.current.call(this, event);
161
175
  };
162
176
 
163
- targetElement.addEventListener(eventName, listener, memoizedOptions);
177
+ unwrappedTarget.addEventListener(eventName, listener, memoizedOptions);
164
178
 
165
- // Remove event listener on cleanup
166
179
  return () => {
167
- targetElement.removeEventListener(eventName, listener, memoizedOptions);
180
+ unwrappedTarget.removeEventListener(eventName, listener, memoizedOptions);
168
181
  };
169
- }, [eventName, element, memoizedOptions]);
170
- }
182
+ }, [unwrappedTarget, eventName, memoizedOptions]);
183
+ } as UseEventListener;
@@ -11,6 +11,55 @@ import type { useReducerWithDeps } from './useReducerWithDeps.js';
11
11
  * This hook is designed in the most general way possible in order to cover all
12
12
  * imaginable use cases.
13
13
  *
14
+ * @example
15
+ * Sometimes, React's immutability constraints mean too much unnecessary copying
16
+ * of data when new data arrives at a high frequency. In such cases, it might be
17
+ * desirable to ignore the constraints by embracing imperative patterns.
18
+ * Here is an example of a scenario where that can make sense:
19
+ *
20
+ * ```tsx
21
+ * type SensorData = { timestamp: number; value: number };
22
+ * const sensorDataRef = useRef<SensorData[]>([]);
23
+ * const mostRecentSensorDataTimestampRef = useRef<number>(0);
24
+ *
25
+ * const [forceUpdate, updateCount] = useForceUpdate();
26
+ * // Limiting the frequency of forced re-renders with some throttle function:
27
+ * const throttledForceUpdateRef = useRef(throttle(forceUpdate));
28
+ *
29
+ * useEffect(() => {
30
+ * return sensorDataObservable.subscribe((data: SensorData) => {
31
+ * // Imagine new sensor data arrives every 1 millisecond. If we were following
32
+ * // React's immutability rules by creating a new array every time, the data
33
+ * // that's already there would have to be copied many times before the new
34
+ * // data would even get a chance to be reflected in the UI for the first time
35
+ * // because it typically takes much longer than 1 millisecond for a new frame
36
+ * // to be displayed. To prevent the waste of computational resources, we just
37
+ * // mutate the existing array every time instead:
38
+ * sensorDataRef.current.push(data);
39
+ * if (data.timestamp > mostRecentSensorDataTimestampRef.current) {
40
+ * mostRecentSensorDataTimestampRef.current = data.timestamp;
41
+ * }
42
+ * throttledForceUpdateRef.current();
43
+ * });
44
+ * }, []);
45
+ *
46
+ * const [timeWindow, setTimeWindow] = useState(1000);
47
+ * const selectedSensorData = useMemo(
48
+ * () => {
49
+ * // Keep this line if you don't want to disable the
50
+ * // react-hooks/exhaustive-deps ESLint rule:
51
+ * updateCount;
52
+ * const threshold = mostRecentSensorDataTimestampRef.current - timeWindow;
53
+ * return sensorDataRef.current.filter(
54
+ * ({ timestamp }) => timestamp >= threshold,
55
+ * );
56
+ * },
57
+ * // sensorDataRef.current always references the same array, so listing it as a
58
+ * // dependency is pointless. Instead, updateCount should be used:
59
+ * [updateCount, timeWindow],
60
+ * );
61
+ * ```
62
+ *
14
63
  * @param callback An optional callback function to call during renders that
15
64
  * were triggered with `forceUpdate()`
16
65
  *
@@ -32,11 +81,11 @@ import type { useReducerWithDeps } from './useReducerWithDeps.js';
32
81
  export function useForceUpdate(callback?: () => void): [() => void, bigint] {
33
82
  // It is very unlikely that the number of updates will exceed
34
83
  // Number.MAX_SAFE_INTEGER, but not impossible. That is why we use bigints.
35
- const [counter, forceUpdate] = useReducer((prev) => prev + 1n, 0n);
36
- const counterRef = useRef(counter);
37
- if (counter !== counterRef.current) {
38
- counterRef.current = counter;
84
+ const [updateCount, forceUpdate] = useReducer((prev) => prev + 1n, 0n);
85
+ const updateCountRef = useRef(updateCount);
86
+ if (updateCount !== updateCountRef.current) {
87
+ updateCountRef.current = updateCount;
39
88
  callback?.();
40
89
  }
41
- return [forceUpdate, counter];
90
+ return [forceUpdate, updateCount];
42
91
  }
@@ -1,4 +1,4 @@
1
- import { type DependencyList, useCallback, useRef } from 'react';
1
+ import { type DependencyList, useRef } from 'react';
2
2
  import { useStateWithDeps } from './useStateWithDeps.js';
3
3
 
4
4
  // We cannot simply import the following types from @types/react since they are
@@ -18,6 +18,9 @@ export type ActionDispatch<ActionArg extends AnyActionArg> = (
18
18
  * `useReducer` hook with an additional dependency array `deps` that resets the
19
19
  * state to `initialState` when dependencies change
20
20
  *
21
+ * For motivation and examples, see
22
+ * https://github.com/facebook/react/issues/33041.
23
+ *
21
24
  * ### On linter support
22
25
  *
23
26
  * The `react-hooks/exhaustive-deps` ESLint rule doesn't support hooks where
@@ -26,7 +29,7 @@ export type ActionDispatch<ActionArg extends AnyActionArg> = (
26
29
  * possible, we don't want to artificially change the parameter's position.
27
30
  * Therefore, there will be no warnings about missing dependencies.
28
31
  * Because of that, additional caution is advised!
29
- * Be sure to check no dependencies are missing from the `deps` array.
32
+ * Be sure to check that no dependencies are missing from the `deps` array.
30
33
  *
31
34
  * Related issue: {@link https://github.com/facebook/react/issues/25443}.
32
35
  *
@@ -57,9 +60,9 @@ export function useReducerWithDeps<S, A extends AnyActionArg>(
57
60
  // Only the initially provided reducer is used
58
61
  const reducerRef = useRef(reducer);
59
62
 
60
- const dispatch = useCallback(function dispatch(...args: A): void {
63
+ const dispatch = useRef(function dispatch(...args: A): void {
61
64
  setState((previousState) => reducerRef.current(previousState, ...args));
62
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
65
+ }).current;
63
66
 
64
67
  return [state, dispatch];
65
68
  }
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  import {
9
- useCallback,
10
9
  useRef,
11
10
  type DependencyList,
12
11
  type Dispatch,
@@ -19,6 +18,41 @@ import { useForceUpdate } from './useForceUpdate.js';
19
18
  * `useState` hook with an additional dependency array `deps` that resets the
20
19
  * state to `initialState` when dependencies change
21
20
  *
21
+ * For motivation and more examples, see
22
+ * https://github.com/facebook/react/issues/33041.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * type Activity = 'breakfast' | 'exercise' | 'swim' | 'board games' | 'dinner';
27
+ *
28
+ * const timeOfDayOptions = ['morning', 'afternoon', 'evening'] as const;
29
+ * type TimeOfDay = (typeof timeOfDayOptions)[number];
30
+ *
31
+ * const activityOptionsByTimeOfDay: {
32
+ * [K in TimeOfDay]: [Activity, ...Activity[]];
33
+ * } = {
34
+ * morning: ['breakfast', 'exercise', 'swim'],
35
+ * afternoon: ['exercise', 'swim', 'board games'],
36
+ * evening: ['board games', 'dinner'],
37
+ * };
38
+ *
39
+ * export function Example() {
40
+ * const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('morning');
41
+ *
42
+ * const activityOptions = activityOptionsByTimeOfDay[timeOfDay];
43
+ * const [activity, setActivity] = useStateWithDeps<Activity>(
44
+ * (prev) => {
45
+ * // Make sure activity is always valid for the current timeOfDay value,
46
+ * // but also don't reset it unless necessary:
47
+ * return prev && activityOptions.includes(prev) ? prev : activityOptions[0];
48
+ * },
49
+ * [activityOptions],
50
+ * );
51
+ *
52
+ * return '...';
53
+ * }
54
+ * ```
55
+ *
22
56
  * @param initialState The value to which the state is set when the component is
23
57
  * mounted or dependencies change
24
58
  *
@@ -32,12 +66,10 @@ export function useStateWithDeps<S>(
32
66
  initialState: S | ((previousState?: S) => S),
33
67
  deps: DependencyList,
34
68
  ): [S, Dispatch<SetStateAction<S>>] {
35
- // It would be possible to use useState instead of
36
- // useRef to store the state, however this would
37
- // trigger re-renders whenever the state is reset due
38
- // to a change in dependencies. In order to avoid these
39
- // re-renders, the state is stored in a ref and an
40
- // update is triggered via forceUpdate below when necessary
69
+ // It would be possible to use useState instead of useRef to store the state,
70
+ // however this would trigger re-renders whenever the state is reset due to a
71
+ // change in dependencies. In order to avoid these re-renders, the state is
72
+ // stored in a ref, and updates are triggered with forceUpdate when necessary.
41
73
  const state = useRef(undefined as S);
42
74
 
43
75
  const prevDeps = useRef(deps);
@@ -59,7 +91,7 @@ export function useStateWithDeps<S>(
59
91
 
60
92
  const [forceUpdate] = useForceUpdate();
61
93
 
62
- const updateState = useCallback(function updateState(
94
+ const updateState = useRef(function updateState(
63
95
  newState: S | ((previousState: S) => S),
64
96
  ): void {
65
97
  let nextState: S;
@@ -72,7 +104,7 @@ export function useStateWithDeps<S>(
72
104
  state.current = nextState;
73
105
  forceUpdate();
74
106
  }
75
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
107
+ }).current;
76
108
 
77
109
  return [state.current, updateState];
78
110
  }