@aweebit/react-essentials 0.5.4 → 0.7.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.
Files changed (59) hide show
  1. package/README.md +836 -0
  2. package/dist/hooks/index.d.ts +5 -0
  3. package/dist/hooks/index.d.ts.map +1 -0
  4. package/dist/hooks/index.js +5 -0
  5. package/dist/hooks/index.js.map +1 -0
  6. package/dist/hooks/useEventListener.d.ts +65 -11
  7. package/dist/hooks/useEventListener.d.ts.map +1 -1
  8. package/dist/hooks/useEventListener.js +49 -18
  9. package/dist/hooks/useEventListener.js.map +1 -1
  10. package/dist/hooks/useForceUpdate.d.ts +52 -4
  11. package/dist/hooks/useForceUpdate.d.ts.map +1 -1
  12. package/dist/hooks/useForceUpdate.js +58 -9
  13. package/dist/hooks/useForceUpdate.js.map +1 -1
  14. package/dist/hooks/useReducerWithDeps.d.ts +17 -11
  15. package/dist/hooks/useReducerWithDeps.d.ts.map +1 -1
  16. package/dist/hooks/useReducerWithDeps.js +14 -12
  17. package/dist/hooks/useReducerWithDeps.js.map +1 -1
  18. package/dist/hooks/useStateWithDeps.d.ts +41 -7
  19. package/dist/hooks/useStateWithDeps.d.ts.map +1 -1
  20. package/dist/hooks/useStateWithDeps.js +47 -15
  21. package/dist/hooks/useStateWithDeps.js.map +1 -1
  22. package/dist/index.d.ts +2 -5
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/misc/createSafeContext.d.ts +85 -0
  27. package/dist/misc/createSafeContext.d.ts.map +1 -0
  28. package/dist/misc/createSafeContext.js +77 -0
  29. package/dist/misc/createSafeContext.js.map +1 -0
  30. package/dist/misc/index.d.ts +2 -0
  31. package/dist/misc/index.d.ts.map +1 -0
  32. package/dist/misc/index.js +2 -0
  33. package/dist/misc/index.js.map +1 -0
  34. package/dist/utils.d.ts +7 -0
  35. package/dist/utils.d.ts.map +1 -0
  36. package/dist/{utils/index.js → utils.js} +1 -2
  37. package/dist/utils.js.map +1 -0
  38. package/package.json +25 -15
  39. package/src/hooks/index.ts +4 -0
  40. package/src/hooks/useEventListener.ts +201 -0
  41. package/src/hooks/useForceUpdate.ts +91 -0
  42. package/{lib → src}/hooks/useReducerWithDeps.ts +27 -18
  43. package/src/hooks/useStateWithDeps.ts +111 -0
  44. package/src/index.ts +2 -0
  45. package/src/misc/createSafeContext.ts +116 -0
  46. package/src/misc/index.ts +1 -0
  47. package/{lib/utils/index.ts → src/utils.ts} +9 -2
  48. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +0 -4
  49. package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +0 -1
  50. package/dist/hooks/useIsomorphicLayoutEffect.js +0 -5
  51. package/dist/hooks/useIsomorphicLayoutEffect.js.map +0 -1
  52. package/dist/utils/index.d.ts +0 -5
  53. package/dist/utils/index.d.ts.map +0 -1
  54. package/dist/utils/index.js.map +0 -1
  55. package/lib/hooks/useEventListener.ts +0 -101
  56. package/lib/hooks/useForceUpdate.ts +0 -40
  57. package/lib/hooks/useIsomorphicLayoutEffect.ts +0 -7
  58. package/lib/hooks/useStateWithDeps.ts +0 -79
  59. package/lib/index.ts +0 -5
@@ -5,29 +5,61 @@
5
5
  * @copyright 2020 Peter Juras
6
6
  */
7
7
  import { useCallback, useRef, } from 'react';
8
- import { depsAreEqual, isFunction } from '../utils/index.js';
9
- import useForceUpdate from './useForceUpdate.js';
8
+ import { depsAreEqual, isFunction } from '../utils.js';
9
+ import { useForceUpdate } from './useForceUpdate.js';
10
10
  /**
11
- * `useState` hook with an additional dependency array that resets the state
12
- * to the `initialState` param when the dependencies passed in the `deps` array
13
- * change
11
+ * `useState` hook with an additional dependency array `deps` that resets the
12
+ * state to `initialState` when dependencies change
14
13
  *
15
- * @param initialState The state that will be set when the component mounts or
16
- * the dependencies change
14
+ * For motivation and more examples, see
15
+ * https://github.com/facebook/react/issues/33041.
17
16
  *
18
- * It can also be a function which returns a state value. If the state is reset
17
+ * @example
18
+ * ```tsx
19
+ * type Activity = 'breakfast' | 'exercise' | 'swim' | 'board games' | 'dinner';
20
+ *
21
+ * const timeOfDayOptions = ['morning', 'afternoon', 'evening'] as const;
22
+ * type TimeOfDay = (typeof timeOfDayOptions)[number];
23
+ *
24
+ * const activityOptionsByTimeOfDay: {
25
+ * [K in TimeOfDay]: [Activity, ...Activity[]];
26
+ * } = {
27
+ * morning: ['breakfast', 'exercise', 'swim'],
28
+ * afternoon: ['exercise', 'swim', 'board games'],
29
+ * evening: ['board games', 'dinner'],
30
+ * };
31
+ *
32
+ * export function Example() {
33
+ * const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('morning');
34
+ *
35
+ * const activityOptions = activityOptionsByTimeOfDay[timeOfDay];
36
+ * const [activity, setActivity] = useStateWithDeps<Activity>(
37
+ * (prev) => {
38
+ * // Make sure activity is always valid for the current timeOfDay value,
39
+ * // but also don't reset it unless necessary:
40
+ * return prev && activityOptions.includes(prev) ? prev : activityOptions[0];
41
+ * },
42
+ * [activityOptions],
43
+ * );
44
+ *
45
+ * return '...';
46
+ * }
47
+ * ```
48
+ *
49
+ * @param initialState The value to which the state is set when the component is
50
+ * mounted or dependencies change
51
+ *
52
+ * It can also be a function that returns a state value. If the state is reset
19
53
  * due to a change of dependencies, this function will be passed the previous
20
54
  * state as its argument (will be `undefined` in the first call upon mount).
21
55
  *
22
56
  * @param deps Dependencies that reset the state to `initialState`
23
57
  */
24
- export default function useStateWithDeps(initialState, deps) {
25
- // It would be possible to use useState instead of
26
- // useRef to store the state, however this would
27
- // trigger re-renders whenever the state is reset due
28
- // to a change in dependencies. In order to avoid these
29
- // re-renders, the state is stored in a ref and an
30
- // update is triggered via forceUpdate below when necessary
58
+ export function useStateWithDeps(initialState, deps) {
59
+ // It would be possible to use useState instead of useRef to store the state,
60
+ // however this would trigger re-renders whenever the state is reset due to a
61
+ // change in dependencies. In order to avoid these re-renders, the state is
62
+ // stored in a ref, and updates are triggered with forceUpdate when necessary.
31
63
  const state = useRef(undefined);
32
64
  const prevDeps = useRef(deps);
33
65
  const isMounted = useRef(false);
@@ -1 +1 @@
1
- {"version":3,"file":"useStateWithDeps.js","sourceRoot":"","sources":["../../lib/hooks/useStateWithDeps.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,WAAW,EACX,MAAM,GAIP,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,cAAc,MAAM,qBAAqB,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CACtC,YAA4C,EAC5C,IAAoB;IAEpB,kDAAkD;IAClD,gDAAgD;IAChD,qDAAqD;IACrD,uDAAuD;IACvD,kDAAkD;IAClD,2DAA2D;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,SAAc,CAAC,CAAC;IAErC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAEhC,mEAAmE;IACnE,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;QAChE,wBAAwB;QACxB,IAAI,SAAY,CAAC;QACjB,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7B,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,SAAS,GAAG,YAAY,CAAC;QAC3B,CAAC;QACD,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;QAC1B,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;QACxB,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,WAAW,CAAC,GAAG,cAAc,EAAE,CAAC;IAEvC,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,WAAW,CAClD,QAAuC;QAEvC,IAAI,SAAY,CAAC;QACjB,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,SAAS,GAAG,QAAQ,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;YACzC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;YAC1B,WAAW,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,kDAAkD;IAE1D,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;AACtC,CAAC"}
1
+ {"version":3,"file":"useStateWithDeps.js","sourceRoot":"","sources":["../../src/hooks/useStateWithDeps.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,WAAW,EACX,MAAM,GAIP,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,MAAM,UAAU,gBAAgB,CAC9B,YAA4C,EAC5C,IAAoB;IAEpB,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,8EAA8E;IAC9E,MAAM,KAAK,GAAG,MAAM,CAAC,SAAc,CAAC,CAAC;IAErC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAEhC,mEAAmE;IACnE,IAAI,CAAC,SAAS,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;QAChE,wBAAwB;QACxB,IAAI,SAAY,CAAC;QACjB,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7B,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,SAAS,GAAG,YAAY,CAAC;QAC3B,CAAC;QACD,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;QAC1B,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;QACxB,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,WAAW,CAAC,GAAG,cAAc,EAAE,CAAC;IAEvC,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,WAAW,CAClD,QAAuC;QAEvC,IAAI,SAAY,CAAC;QACjB,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,SAAS,GAAG,QAAQ,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;YACzC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;YAC1B,WAAW,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,kDAAkD;IAE1D,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;AACtC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- export { default as useEventListener } from './hooks/useEventListener.js';
2
- export { default as useForceUpdate } from './hooks/useForceUpdate.js';
3
- export { default as useIsomorphicLayoutEffect } from './hooks/useIsomorphicLayoutEffect.js';
4
- export { default as useReducerWithDeps } from './hooks/useReducerWithDeps.js';
5
- export { default as useStateWithDeps } from './hooks/useStateWithDeps.js';
1
+ export * from './hooks/index.js';
2
+ export * from './misc/index.js';
6
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAC5F,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- export { default as useEventListener } from './hooks/useEventListener.js';
2
- export { default as useForceUpdate } from './hooks/useForceUpdate.js';
3
- export { default as useIsomorphicLayoutEffect } from './hooks/useIsomorphicLayoutEffect.js';
4
- export { default as useReducerWithDeps } from './hooks/useReducerWithDeps.js';
5
- export { default as useStateWithDeps } from './hooks/useStateWithDeps.js';
1
+ export * from './hooks/index.js';
2
+ export * from './misc/index.js';
6
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAC5F,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,85 @@
1
+ import { type Context, type Provider } from 'react';
2
+ import type { ArgumentFallback } from '../utils.js';
3
+ /**
4
+ * A React context with a required `displayName` and the obsolete `Consumer`
5
+ * property purposefully omitted so that it is impossible to pass the context
6
+ * as an argument to `useContext` or `use` (the hook produced with
7
+ * {@linkcode createSafeContext} should be used instead)
8
+ *
9
+ * @see {@linkcode createSafeContext}
10
+ */
11
+ export type RestrictedContext<T> = Context<T> extends Provider<T> ? {
12
+ Provider: Provider<T>;
13
+ displayName: string;
14
+ } & Provider<T> : {
15
+ Provider: Provider<T>;
16
+ displayName: string;
17
+ };
18
+ /**
19
+ * The return type of {@linkcode createSafeContext}
20
+ *
21
+ * @see {@linkcode createSafeContext}
22
+ */
23
+ export type SafeContext<DisplayName extends string, T> = {
24
+ [K in `${DisplayName}Context`]: RestrictedContext<T>;
25
+ } & {
26
+ [K in `use${DisplayName}`]: () => T;
27
+ };
28
+ /**
29
+ * For a given type `T`, returns a function that produces both a context of that
30
+ * type and a hook that returns the current context value if one was provided,
31
+ * or throws an error otherwise
32
+ *
33
+ * The advantages over vanilla `createContext` are that no default value has to
34
+ * be provided, and that a meaningful context name is displayed in dev tools
35
+ * instead of generic `Context.Provider`.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * enum Direction {
40
+ * Up,
41
+ * Down,
42
+ * Left,
43
+ * Right,
44
+ * }
45
+ *
46
+ * // Before
47
+ * const DirectionContext = createContext<Direction | undefined>(undefined);
48
+ * DirectionContext.displayName = 'DirectionContext';
49
+ *
50
+ * const useDirection = () => {
51
+ * const direction = useContext(DirectionContext);
52
+ * if (direction === undefined) {
53
+ * // Called outside of a <DirectionContext.Provider> boundary!
54
+ * // Or maybe undefined was explicitly provided as the context value
55
+ * // (ideally that shouldn't be allowed, but it is because we had to include
56
+ * // undefined in the context type so as to provide a meaningful default)
57
+ * throw new Error('No DirectionContext value was provided');
58
+ * }
59
+ * // Thanks to the undefined check, the type is now narrowed down to Direction
60
+ * return direction;
61
+ * };
62
+ *
63
+ * // After
64
+ * const { DirectionContext, useDirection } =
65
+ * createSafeContext<Direction>()('Direction'); // That's it :)
66
+ *
67
+ * const Parent = () => (
68
+ * // Providing undefined as the value is not allowed 👍
69
+ * <Direction.Provider value={Direction.Up}>
70
+ * <Child />
71
+ * </Direction.Provider>
72
+ * );
73
+ *
74
+ * const Child = () => `Current direction: ${Direction[useDirection()]}`;
75
+ * ```
76
+ *
77
+ * @returns
78
+ * A function that accepts a single string argument `displayName` (e.g.
79
+ * `"Direction"`) and returns an object with the following properties:
80
+ * - ``` `${displayName}Context` ``` (e.g. `DirectionContext`): the context
81
+ * - ``` `use${displayName}` ``` (e.g. `useDirection`): a hook that returns the
82
+ * current context value if one was provided, or throws an error otherwise
83
+ */
84
+ export declare function createSafeContext<T = never>(): <DisplayName extends string>(displayName: [T] extends [never] ? never : ArgumentFallback<DisplayName, never, string>) => SafeContext<DisplayName, T>;
85
+ //# sourceMappingURL=createSafeContext.d.ts.map
@@ -0,0 +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;;;;GAIG;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;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"}
@@ -0,0 +1,77 @@
1
+ import { createContext, useContext } from 'react';
2
+ const moValueSymbol = Symbol('noValue');
3
+ /**
4
+ * For a given type `T`, returns a function that produces both a context of that
5
+ * type and a hook that returns the current context value if one was provided,
6
+ * or throws an error otherwise
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
+ *
12
+ * @example
13
+ * ```tsx
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 :)
41
+ *
42
+ * const Parent = () => (
43
+ * // Providing undefined as the value is not allowed 👍
44
+ * <Direction.Provider value={Direction.Up}>
45
+ * <Child />
46
+ * </Direction.Provider>
47
+ * );
48
+ *
49
+ * const Child = () => `Current direction: ${Direction[useDirection()]}`;
50
+ * ```
51
+ *
52
+ * @returns
53
+ * A function that accepts a single string argument `displayName` (e.g.
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
57
+ * current context value if one was provided, or throws an error otherwise
58
+ */
59
+ export function createSafeContext() {
60
+ return (displayName) => {
61
+ const contextName = `${displayName}Context`;
62
+ const hookName = `use${displayName}`;
63
+ const Context = createContext(moValueSymbol);
64
+ Context.displayName = contextName;
65
+ return {
66
+ [contextName]: Context,
67
+ [hookName]: () => {
68
+ const value = useContext(Context);
69
+ if (value === moValueSymbol) {
70
+ throw new Error(`No ${contextName} value was provided`);
71
+ }
72
+ return value;
73
+ },
74
+ };
75
+ };
76
+ }
77
+ //# sourceMappingURL=createSafeContext.js.map
@@ -0,0 +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;AA6BxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;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"}
@@ -0,0 +1,2 @@
1
+ export * from './createSafeContext.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/misc/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './createSafeContext.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/misc/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { DependencyList } from 'react';
2
+ export type Callable = (...args: never) => unknown;
3
+ export type ArgumentFallback<T extends Base, Default extends Base, Base = unknown> = [T] extends [never] ? Default : [Base] extends [T] ? Default : T;
4
+ export declare function noop(): void;
5
+ export declare function isFunction(input: unknown): input is Callable;
6
+ export declare function depsAreEqual(prevDeps: DependencyList, deps: DependencyList): boolean;
7
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE5C,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC;AAEnD,MAAM,MAAM,gBAAgB,CAC1B,CAAC,SAAS,IAAI,EACd,OAAO,SAAS,IAAI,EACpB,IAAI,GAAG,OAAO,IACZ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC;AAErE,wBAAgB,IAAI,SAAK;AAEzB,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D;AAED,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,cAAc,GACnB,OAAO,CAKT"}
@@ -1,5 +1,4 @@
1
1
  export function noop() { }
2
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
3
2
  export function isFunction(input) {
4
3
  return typeof input === 'function';
5
4
  }
@@ -7,4 +6,4 @@ export function depsAreEqual(prevDeps, deps) {
7
6
  return (prevDeps.length === deps.length &&
8
7
  deps.every((dep, index) => Object.is(dep, prevDeps[index])));
9
8
  }
10
- //# sourceMappingURL=index.js.map
9
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,IAAI,KAAI,CAAC;AAEzB,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,QAAwB,EACxB,IAAoB;IAEpB,OAAO,CACL,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;QAC/B,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAC5D,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aweebit/react-essentials",
3
- "version": "0.5.4",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "repository": "github:aweebit/react-essentials",
6
6
  "main": "dist/index.js",
@@ -10,28 +10,38 @@
10
10
  "prettier": "npm run prettier:base -- .",
11
11
  "lint": "eslint --max-warnings=0",
12
12
  "build": "rimraf dist && tsc -b -f",
13
- "prepare": "husky && npm run build"
14
- },
15
- "lint-staged": {
16
- "!(*.{jsx,jsx,ts,tsx})": "npm run prettier -- --ignore-unknown",
17
- "**.{jsx,jsx,ts,tsx}": "npm run lint --"
13
+ "doc": "rimraf README.md && typedoc",
14
+ "prepack": "npm run build && npm run doc",
15
+ "prepare": "husky"
18
16
  },
19
17
  "peerDependencies": {
20
- "react": ">=18.0.0 <20"
18
+ "@types/react": ">=18",
19
+ "react": ">=18",
20
+ "typescript": ">=5.4"
21
21
  },
22
- "dependencies": {
23
- "@types/react": "19.1.6"
22
+ "peerDependenciesMeta": {
23
+ "@types/react": {
24
+ "optional": true
25
+ },
26
+ "typescript": {
27
+ "optional": true
28
+ }
24
29
  },
25
30
  "devDependencies": {
26
- "@eslint/js": "^9.28.0",
27
- "eslint": "^9.28.0",
31
+ "@eslint/js": "^9.36.0",
32
+ "@types/react": "^18.3.24",
33
+ "eslint": "^9.36.0",
34
+ "eslint-config-prettier": "^10.1.8",
28
35
  "eslint-plugin-react-hooks": "^5.2.0",
29
36
  "husky": "^9.1.7",
30
- "lint-staged": "^16.1.0",
31
- "prettier": "3.5.3",
37
+ "lint-staged": "^16.1.6",
38
+ "prettier": "3.6.2",
39
+ "react": "^18.3.1",
32
40
  "rimraf": "^6.0.1",
33
- "typescript": "~5.8.3",
34
- "typescript-eslint": "^8.33.1"
41
+ "typedoc": "^0.28.13",
42
+ "typedoc-plugin-markdown": "^4.9.0",
43
+ "typescript": "~5.9.2",
44
+ "typescript-eslint": "^8.44.0"
35
45
  },
36
46
  "license": "MIT"
37
47
  }
@@ -0,0 +1,4 @@
1
+ export * from './useEventListener.js';
2
+ export * from './useForceUpdate.js';
3
+ export * from './useReducerWithDeps.js';
4
+ export * from './useStateWithDeps.js';
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @file Based on {@link https://github.com/juliencrn/usehooks-ts}
3
+ *
4
+ * @license MIT
5
+ * @copyright 2020 Julien CARON
6
+ */
7
+
8
+ import { useEffect, useMemo, useRef } from 'react';
9
+
10
+ type UseEventListenerOverloadArgs<
11
+ EventMap,
12
+ K extends keyof EventMap,
13
+ T extends EventTarget,
14
+ > = [
15
+ target: T | null,
16
+ eventName: K,
17
+ handler: (this: NoInfer<T>, event: EventMap[K]) => void,
18
+ options?: AddEventListenerOptions | boolean,
19
+ ];
20
+
21
+ /**
22
+ * Adds `handler` as a listener for the event `eventName` of `target` with the
23
+ * provided `options` applied
24
+ *
25
+ * If `target` is not provided, `window` is used instead.
26
+ *
27
+ * If `target` is `null`, no event listener is added. This is useful when
28
+ * working with DOM element refs, or when the event listener needs to be removed
29
+ * temporarily.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * useEventListener('resize', () => {
34
+ * console.log(window.innerWidth, window.innerHeight);
35
+ * });
36
+ *
37
+ * useEventListener(document, 'visibilitychange', () => {
38
+ * console.log(document.visibilityState);
39
+ * });
40
+ *
41
+ * const buttonRef = useRef<HTMLButtonElement>(null);
42
+ * useEventListener(buttonRef.current, 'click', () => console.log('click'));
43
+ * ```
44
+ *
45
+ * @ignore
46
+ */
47
+ export function useEventListener<K extends keyof WindowEventMap>(
48
+ eventName: K,
49
+ handler: (this: Window, event: WindowEventMap[K]) => void,
50
+ options?: AddEventListenerOptions | boolean,
51
+ ): void;
52
+
53
+ /**
54
+ * @see {@linkcode useEventListener}
55
+ * @ignore
56
+ */
57
+ export function useEventListener<K extends keyof WindowEventMap>(
58
+ ...args: UseEventListenerOverloadArgs<WindowEventMap, K, Window>
59
+ ): void;
60
+
61
+ /**
62
+ * @see {@linkcode useEventListener}
63
+ * @ignore
64
+ */
65
+ export function useEventListener<K extends keyof DocumentEventMap>(
66
+ ...args: UseEventListenerOverloadArgs<DocumentEventMap, K, Document>
67
+ ): void;
68
+
69
+ /**
70
+ * @see {@linkcode useEventListener}
71
+ * @ignore
72
+ */
73
+ export function useEventListener<
74
+ K extends keyof HTMLElementEventMap,
75
+ T extends HTMLElement,
76
+ >(...args: UseEventListenerOverloadArgs<HTMLElementEventMap, K, T>): void;
77
+
78
+ /**
79
+ * @see {@linkcode useEventListener}
80
+ * @ignore
81
+ */
82
+ export function useEventListener<
83
+ K extends keyof SVGElementEventMap,
84
+ T extends SVGElement,
85
+ >(...args: UseEventListenerOverloadArgs<SVGElementEventMap, K, T>): void;
86
+
87
+ /**
88
+ * @see {@linkcode useEventListener}
89
+ * @ignore
90
+ */
91
+ export function useEventListener<
92
+ K extends keyof MathMLElementEventMap,
93
+ T extends MathMLElement,
94
+ >(...args: UseEventListenerOverloadArgs<MathMLElementEventMap, K, T>): void;
95
+
96
+ /**
97
+ * @see {@linkcode useEventListener}
98
+ */
99
+ export function useEventListener<K extends keyof WindowEventMap>(
100
+ eventName: K,
101
+ handler: (this: Window, event: WindowEventMap[K]) => void,
102
+ options?: AddEventListenerOptions | boolean,
103
+ ): void;
104
+
105
+ /**
106
+ * @see {@linkcode useEventListener}
107
+ */
108
+ export function useEventListener<T extends EventTarget>(
109
+ target: T | null,
110
+ eventName: string,
111
+ handler: (this: NoInfer<T>, event: Event) => void,
112
+ options?: AddEventListenerOptions | boolean,
113
+ ): void;
114
+
115
+ /**
116
+ * Adds `handler` as a listener for the event `eventName` of `target` with the
117
+ * provided `options` applied
118
+ *
119
+ * If `target` is not provided, `window` is used instead.
120
+ *
121
+ * If `target` is `null`, no event listener is added. This is useful when
122
+ * working with DOM element refs, or when the event listener needs to be removed
123
+ * temporarily.
124
+ *
125
+ * @example
126
+ * ```tsx
127
+ * useEventListener('resize', () => {
128
+ * console.log(window.innerWidth, window.innerHeight);
129
+ * });
130
+ *
131
+ * useEventListener(document, 'visibilitychange', () => {
132
+ * console.log(document.visibilityState);
133
+ * });
134
+ *
135
+ * const buttonRef = useRef<HTMLButtonElement>(null);
136
+ * useEventListener(buttonRef.current, 'click', () => console.log('click'));
137
+ * ```
138
+ */
139
+ export function useEventListener(
140
+ ...args: UseEventListenerArgsWithoutTarget | UseEventListenerArgsWithTarget
141
+ ) {
142
+ let eventName: string;
143
+ let handler: (this: EventTarget, event: Event) => void;
144
+ let target: EventTarget | null | undefined;
145
+ let options: AddEventListenerOptions | boolean | undefined;
146
+
147
+ if (typeof args[0] === 'string') {
148
+ [eventName, handler, options] = args as UseEventListenerArgsWithoutTarget;
149
+ } else {
150
+ [target, eventName, handler, options] =
151
+ args as UseEventListenerArgsWithTarget;
152
+ }
153
+
154
+ const handlerRef = useRef(handler);
155
+ handlerRef.current = handler;
156
+
157
+ const {
158
+ capture = false,
159
+ once = false,
160
+ passive,
161
+ signal,
162
+ } = typeof options === 'boolean' ? { capture: options } : (options ?? {});
163
+
164
+ const memoizedOptions = useMemo(
165
+ () => options,
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ [capture, once, passive, signal],
168
+ );
169
+
170
+ useEffect(() => {
171
+ if (target === null) {
172
+ // No element has been attached to the ref yet
173
+ return;
174
+ }
175
+
176
+ const definedTarget = target ?? window;
177
+
178
+ const listener: typeof handler = function (event) {
179
+ handlerRef.current.call(this, event);
180
+ };
181
+
182
+ definedTarget.addEventListener(eventName, listener, memoizedOptions);
183
+
184
+ return () => {
185
+ definedTarget.removeEventListener(eventName, listener, memoizedOptions);
186
+ };
187
+ }, [eventName, target, memoizedOptions]);
188
+ }
189
+
190
+ type UseEventListenerArgsWithoutTarget = [
191
+ eventName: string,
192
+ handler: (event: Event) => void,
193
+ options?: AddEventListenerOptions | boolean | undefined,
194
+ ];
195
+
196
+ type UseEventListenerArgsWithTarget = [
197
+ target: EventTarget | null,
198
+ eventName: string,
199
+ handler: (event: Event) => void,
200
+ options?: AddEventListenerOptions | boolean | undefined,
201
+ ];