@hrnec06/react_utils 1.6.0 → 1.7.1

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 (28) hide show
  1. package/README.md +1 -0
  2. package/package.json +2 -2
  3. package/src/components/ContextMenu/ContextMenu.tsx +235 -0
  4. package/src/components/ContextMenu/ContextMenu.types.ts +17 -0
  5. package/src/components/ContextMenu/ContextMenuCtx.tsx +33 -0
  6. package/src/components/ContextMenu/ContextMenuItem.tsx +147 -0
  7. package/src/components/ContextMenu/ContextMenuRoot.tsx +28 -0
  8. package/src/components/ContextMenu/ContextMenuSection.tsx +28 -0
  9. package/src/components/Debugger/Debugger.tsx +70 -18
  10. package/src/components/Debugger/DebuggerTerminal.tsx +3 -3
  11. package/src/components/Debugger/parser/DebugParser.tsx +117 -14
  12. package/src/components/Debugger/parser/DebugTerminal.tsx +41 -7
  13. package/src/components/Debugger/parser/ValueArray.tsx +31 -7
  14. package/src/components/Debugger/parser/ValueBoolean.tsx +15 -4
  15. package/src/components/Debugger/parser/ValueConstant.tsx +21 -0
  16. package/src/components/Debugger/parser/ValueFunction.tsx +17 -5
  17. package/src/components/Debugger/parser/ValueNumber.tsx +14 -4
  18. package/src/components/Debugger/parser/ValueObject.tsx +69 -17
  19. package/src/components/Debugger/parser/ValueString.tsx +13 -4
  20. package/src/components/ResizeableBox/ResizeableBox.tsx +1 -1
  21. package/src/hooks/useDebounce.ts +26 -0
  22. package/src/hooks/useEvent.ts +15 -0
  23. package/src/hooks/useLatestRef.ts +12 -0
  24. package/src/hooks/useSyncRef.ts +17 -0
  25. package/src/hooks/useTransition.ts +2 -1
  26. package/src/index.ts +12 -3
  27. package/src/lib/errors/ContextError.ts +11 -0
  28. package/src/hooks/useUpdatedRef.ts +0 -12
@@ -1,3 +1,4 @@
1
+ import { forwardRef, RefObject, useDeferredValue, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
1
2
  import useDebugger from "../DebuggerContext";
2
3
  import { matchPath } from "../DebuggerLogic";
3
4
  import { Terminal } from "../DebuggerTerminal";
@@ -5,46 +6,148 @@ import DebugTerminal from "./DebugTerminal";
5
6
  import ValueArray from "./ValueArray";
6
7
  import ValueBoolean from "./ValueBoolean";
7
8
  import ValueFunction from "./ValueFunction";
8
- import ValueKeyword from "./ValueKeyword";
9
9
  import ValueNumber from "./ValueNumber";
10
10
  import ValueObject from "./ValueObject";
11
11
  import ValueString from "./ValueString";
12
12
  import ValueSymbol from "./ValueSymbol";
13
+ import { Nullable } from "@hrnec06/util";
14
+ import ContextMenu from "../../ContextMenu/ContextMenu";
15
+ import ValueConstant from "./ValueConstant";
16
+ import useSyncRef from "../../../hooks/useSyncRef";
17
+ import CTXM from "../../ContextMenu/ContextMenu.types";
18
+ import clsx from "clsx";
19
+ import useDebounce from "../../../hooks/useDebounce";
20
+ import { useCTXMListener } from "../../ContextMenu/ContextMenuCtx";
13
21
 
14
- interface ParseValueProps {
22
+ export interface ParseValueRef {
23
+ copyValue?: string,
24
+ name?: string,
25
+ menu?: CTXM.ContextMenuSection,
26
+ }
27
+
28
+ interface ClassifyProps {
29
+ ref: React.Ref<ParseValueRef>,
15
30
  value: unknown,
16
31
  path?: string[]
17
32
  }
18
- export default function ParseValue({
33
+ function Classify({
34
+ ref,
19
35
  value,
20
36
  path = []
21
- }: ParseValueProps) {
37
+ }: ClassifyProps) {
22
38
  const debug = useDebugger();
23
39
 
24
40
  switch (typeof value) {
25
- case 'string': return (<ValueString value={value} />);
41
+ case 'string': return (<ValueString ref={ref} value={value} />);
26
42
  case 'bigint':
27
- case 'number': return (<ValueNumber value={value} />);
28
- case 'boolean': return (<ValueBoolean value={value} />);
29
- case 'undefined': return (<ValueKeyword value={"undefined"} />);
30
- case 'function': return (<ValueFunction value={value} />);
43
+ case 'number': return (<ValueNumber ref={ref} value={value} />);
44
+ case 'boolean': return (<ValueBoolean ref={ref} value={value} />);
45
+ case 'undefined': return (<ValueConstant ref={ref} value="undefined" />)
46
+ case 'function': return (<ValueFunction ref={ref} value={value} />);
31
47
  case 'symbol': return (<ValueSymbol value={value} />)
32
48
  case 'object': {
33
49
  if (value === null)
34
- return (<ValueKeyword value={"null"} />)
50
+ return (<ValueConstant ref={ref} value={"null"} />)
35
51
 
36
52
  const isRootObject = path.length === 0;
37
53
 
38
54
  const shouldBeExpanded = (isRootObject && debug.options.openRoot) || (matchPath(path, debug.paths.open) && !matchPath(path, debug.paths.exclude));
39
55
 
40
56
  if (value instanceof Terminal)
41
- return <DebugTerminal terminal={value} path={path} defaultExpanded={shouldBeExpanded} />
57
+ return <DebugTerminal ref={ref} terminal={value} path={path} defaultExpanded={shouldBeExpanded} />
42
58
 
43
59
  if (Array.isArray(value)) {
44
- return <ValueArray value={value} path={path} defaultExpanded={shouldBeExpanded} />
60
+ return <ValueArray ref={ref} value={value} path={path} defaultExpanded={shouldBeExpanded} />
45
61
  }
46
62
 
47
- return <ValueObject value={value} path={path} defaultExpanded={shouldBeExpanded} />
63
+ return <ValueObject ref={ref} value={value} path={path} defaultExpanded={shouldBeExpanded} />
48
64
  }
49
65
  }
50
- }
66
+ }
67
+
68
+ interface ParseValueProps {
69
+ value: unknown,
70
+ path?: string[],
71
+ /**
72
+ * Children to be put before the parsed value
73
+ */
74
+ children?: React.ReactNode
75
+ }
76
+ function ParseValue({
77
+ value,
78
+ path = [],
79
+ children
80
+ }: ParseValueProps)
81
+ {
82
+ const valueRef = useRef<ParseValueRef>(null);
83
+ const [localValue, setLocalValue] = useState<Nullable<ParseValueRef>>(null);
84
+ const syncedValueRef = useSyncRef(valueRef, setLocalValue);
85
+
86
+ return (
87
+ <DebugCTXMProvider
88
+ path={path}
89
+ ref={localValue}
90
+ >
91
+ {children}
92
+
93
+ <Classify
94
+ ref={syncedValueRef}
95
+ value={value}
96
+ path={path}
97
+ />
98
+ </DebugCTXMProvider>
99
+ );
100
+ }
101
+
102
+ interface DebugCTXMProviderProps extends React.PropsWithChildren {
103
+ path: string[],
104
+ ref: Nullable<ParseValueRef>
105
+ }
106
+ export function DebugCTXMProvider({
107
+ path,
108
+ ref,
109
+ children
110
+ }: DebugCTXMProviderProps)
111
+ {
112
+ const ctxmLabel = useMemo(() => {
113
+ if (!path.length)
114
+ return "root";
115
+
116
+ return path.join('.');
117
+ }, [path]);
118
+
119
+ const handleCopy = (ref === null || ref.copyValue === undefined)
120
+ ? undefined
121
+ : () => navigator.clipboard.writeText(ref.copyValue!);
122
+
123
+ return (
124
+ <ContextMenu
125
+ className={clsx(
126
+ "inline align-middle h-full",
127
+ "outline-sky-500 outline-offset-2",
128
+ )}
129
+
130
+ $autoGenerateWrapper
131
+ $id={ctxmLabel}
132
+ $inherit={(menu) => ({
133
+ parent: menu.id,
134
+ children: menu.menu
135
+ })}
136
+ $menu={[
137
+ [
138
+ {
139
+ id: 'copy',
140
+ label: "Copy",
141
+ description: ref?.name,
142
+ onClick: handleCopy
143
+ },
144
+ ],
145
+ ref?.menu ?? null
146
+ ]}
147
+ >
148
+ {children}
149
+ </ContextMenu>
150
+ )
151
+ }
152
+
153
+ export default ParseValue;
@@ -1,8 +1,8 @@
1
- import { createContext, useContext, useLayoutEffect, useRef, useState } from "react";
1
+ import { createContext, forwardRef, useContext, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
2
2
  import { LogItem, LogRole, LogType, Terminal } from "../DebuggerTerminal"
3
3
  import clsx from "clsx";
4
4
  import { KeyboardButton, matchBool, matchKeyEvent, matchMouseEvent, MouseButton, Optional } from "@hrnec06/util";
5
- import ParseValue from "./DebugParser";
5
+ import ParseValue, { ParseValueRef } from "./DebugParser";
6
6
  import { ChevronRight, CircleAlert, SendIcon, TerminalIcon, Trash, TriangleAlert } from "lucide-react";
7
7
  import ResizeableBox from "../../ResizeableBox/ResizeableBox";
8
8
  import { Chevron_Toggle } from "../DebuggerSymbols";
@@ -28,10 +28,42 @@ interface DebugTerminalProps {
28
28
  path: string[],
29
29
  defaultExpanded?: boolean
30
30
  }
31
- export default function DebugTerminal({ terminal, path, defaultExpanded }: DebugTerminalProps)
32
- {
31
+ const DebugTerminal = forwardRef<ParseValueRef, DebugTerminalProps>(({
32
+ terminal,
33
+ path,
34
+ defaultExpanded
35
+ }, ref) => {
33
36
  const [showing, setShowing] = useState(!!defaultExpanded);
34
37
 
38
+ useImperativeHandle(ref, () => {
39
+ const logToString = (log: LogItem) => {
40
+ let value: string;
41
+
42
+ try
43
+ {
44
+ value = JSON.stringify(log.value);
45
+ }
46
+ catch (error)
47
+ {
48
+ value = `Error parsing value (${typeof log.value})`;
49
+ }
50
+
51
+ return `${log.role === LogRole.Input ? '> ' : `${LogType[log.type]}: `}${value}`;
52
+ }
53
+
54
+ return {
55
+ name: `Terminal ${terminal.name}`,
56
+ copyValue: terminal.getLog().reduce((prev, curr) => prev + logToString(curr) + "\n", ""),
57
+ menu: [
58
+ {
59
+ id: "terminal-clear",
60
+ label: "Clear",
61
+ onClick: () => terminal.clear(),
62
+ }
63
+ ]
64
+ };
65
+ }, [terminal.getLog()]);
66
+
35
67
  const handleToggle = (value: boolean) => {
36
68
  setShowing(value);
37
69
  }
@@ -60,8 +92,8 @@ export default function DebugTerminal({ terminal, path, defaultExpanded }: Debug
60
92
 
61
93
  <TerminalMain show={showing} />
62
94
  </LocalTerminalContext.Provider>
63
- )
64
- }
95
+ );
96
+ });
65
97
 
66
98
  interface TerminalMainProps {
67
99
  show: boolean
@@ -338,4 +370,6 @@ function TerminalInput()
338
370
  </button>
339
371
  </div>
340
372
  )
341
- }
373
+ }
374
+
375
+ export default DebugTerminal;
@@ -1,23 +1,46 @@
1
- import React, { useLayoutEffect, useMemo, useState } from "react";
1
+ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useState } from "react";
2
2
  import useDebugger from "../DebuggerContext";
3
3
  import { Char_Bracket, Char_Comma, Chevron_Toggle } from "../DebuggerSymbols";
4
4
  import ParseValueSimple from "./DebugParserSimple";
5
- import ParseValue from "./DebugParser";
5
+ import ParseValue, { ParseValueRef } from "./DebugParser";
6
+ import { Optional } from "@hrnec06/util";
6
7
 
7
8
  interface ValueArrayProps {
8
9
  value: unknown[],
9
10
  path: string[],
10
11
  defaultExpanded?: boolean
11
12
  }
12
- export default function ValueArray({
13
+ const ValueArray = forwardRef<ParseValueRef, ValueArrayProps>(({
13
14
  value,
14
15
  path,
15
16
  defaultExpanded = false
16
- }: ValueArrayProps) {
17
+ }, ref) => {
17
18
  const debug = useDebugger();
18
19
 
19
20
  const [expanded, setExpanded] = useState(defaultExpanded);
20
21
 
22
+ useImperativeHandle(ref, () => {
23
+ let copyValue: Optional<string> = undefined;
24
+ try
25
+ {
26
+ copyValue = JSON.stringify(value);
27
+ } catch {}
28
+
29
+ return {
30
+ name: `Array (${value.length})`,
31
+ copyValue: copyValue,
32
+ menu: [
33
+ {
34
+ id: "open-toggle",
35
+ label: expanded ? "Collapse" : "Expand",
36
+ onClick: () => {
37
+ setExpanded(!expanded);
38
+ }
39
+ }
40
+ ]
41
+ };
42
+ }, [value, expanded]);
43
+
21
44
  const children = useMemo(() => {
22
45
  const children: React.ReactNode[] = [];
23
46
 
@@ -106,7 +129,6 @@ export default function ValueArray({
106
129
  onClick={() => setExpanded(true)}
107
130
  >
108
131
  <Char_Bracket text="[" />
109
- {/* <span className="text-zinc-500">...</span> */}
110
132
  {collapsedPreview}
111
133
  <Char_Bracket text="]" />
112
134
  </div>
@@ -114,7 +136,7 @@ export default function ValueArray({
114
136
  }
115
137
  </>
116
138
  );
117
- }
139
+ });
118
140
 
119
141
  interface CompactArrayItemProps {
120
142
  value: unknown,
@@ -140,4 +162,6 @@ function CompactArrayItem({
140
162
  {isLast && (<Char_Comma />)}
141
163
  </>
142
164
  )
143
- }
165
+ }
166
+
167
+ export default ValueArray;
@@ -1,10 +1,21 @@
1
+ import { forwardRef, useImperativeHandle } from "react"
1
2
  import ValueKeyword from "./ValueKeyword"
3
+ import { ParseValueRef } from "./DebugParser";
2
4
 
3
5
  interface ValueBooleanProps {
4
6
  value: boolean
5
7
  }
6
- export default function ValueBoolean({
8
+ const ValueBoolean = forwardRef<ParseValueRef, ValueBooleanProps>(({
7
9
  value
8
- }: ValueBooleanProps) {
9
- return <ValueKeyword value={value ? 'true' : 'false'} />
10
- }
10
+ }, ref) => {
11
+ useImperativeHandle(ref, () => ({
12
+ name: "Boolean",
13
+ copyValue: value ? "true" : "false"
14
+ }), [value]);
15
+
16
+ return (
17
+ <ValueKeyword value={value ? 'true' : 'false'} />
18
+ )
19
+ });
20
+
21
+ export default ValueBoolean;
@@ -0,0 +1,21 @@
1
+ import { forwardRef, useImperativeHandle } from "react";
2
+ import ValueKeyword from "./ValueKeyword";
3
+ import { ParseValueRef } from "./DebugParser";
4
+
5
+ interface ValueConstantProps {
6
+ value: string
7
+ }
8
+ const ValueConstant = forwardRef<ParseValueRef, ValueConstantProps>(({
9
+ value
10
+ }, ref) => {
11
+ useImperativeHandle(ref, () => ({
12
+ name: value,
13
+ copyValue: value
14
+ }), [value]);
15
+
16
+ return (
17
+ <ValueKeyword value={value} />
18
+ )
19
+ });
20
+
21
+ export default ValueConstant;
@@ -1,10 +1,18 @@
1
+ import { forwardRef, useImperativeHandle } from "react";
2
+ import { ParseValueRef } from "./DebugParser";
3
+
1
4
  interface ValueFunctionProps {
2
5
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
3
6
  value: Function
4
7
  }
5
- export default function ValueFunction({
8
+ const ValueFunction = forwardRef<ParseValueRef, ValueFunctionProps>(({
6
9
  value
7
- }: ValueFunctionProps) {
10
+ }, ref) => {
11
+ useImperativeHandle(ref, () => ({
12
+ name: "Function",
13
+ copyValue: value.toString()
14
+ }), [value]);
15
+
8
16
  const argsMatch = value.toString().match(/^[^(]*\(\s*([^)]*)\)/);
9
17
  let args: string = '';
10
18
 
@@ -12,14 +20,18 @@ export default function ValueFunction({
12
20
  args = argsMatch[1]!;
13
21
  }
14
22
 
23
+ const displayValue = `${value.name}(${args})`;
24
+
15
25
  return (
16
26
  <>
17
27
  <span className="text-[#f2824a]">
18
28
  {'ƒ '}
19
29
  </span>
20
30
  <span className="text-white">
21
- {`${value.name}(${args})`}
31
+ {displayValue}
22
32
  </span>
23
33
  </>
24
- )
25
- }
34
+ );
35
+ });
36
+
37
+ export default ValueFunction;
@@ -1,12 +1,22 @@
1
+ import { forwardRef, useImperativeHandle } from "react"
2
+ import { ParseValueRef } from "./DebugParser";
3
+
1
4
  interface ValueNumberProps {
2
5
  value: number | bigint | string
3
6
  }
4
- export default function ValueNumber({
7
+ const ValueNumber = forwardRef<ParseValueRef, ValueNumberProps>(({
5
8
  value
6
- }: ValueNumberProps) {
9
+ }, ref) => {
10
+ useImperativeHandle(ref, () => ({
11
+ name: "Number",
12
+ copyValue: value.toString()
13
+ }), [value]);
14
+
7
15
  return (
8
16
  <span className="text-[#a7ce9b]">
9
17
  {value}
10
18
  </span>
11
- )
12
- }
19
+ );
20
+ });
21
+
22
+ export default ValueNumber;
@@ -1,25 +1,49 @@
1
- import React, { useLayoutEffect, useMemo, useState } from "react";
1
+ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import useDebugger from "../DebuggerContext";
3
- import { entries, keys } from "@hrnec06/util";
3
+ import { entries, keys, Nullable, Optional } from "@hrnec06/util";
4
4
  import { Char_Bracket, Char_Colon, Char_Comma, Chevron_Toggle } from "../DebuggerSymbols";
5
- import ParseValue from "./DebugParser";
5
+ import ParseValue, { DebugCTXMProvider, ParseValueRef } from "./DebugParser";
6
+ import useSyncRef from "../../../hooks/useSyncRef";
7
+ import { useCTXMListener } from "../../ContextMenu/ContextMenuCtx";
6
8
 
7
9
  interface ValueObjectProps {
8
10
  value: object,
9
11
  path: string[],
10
12
  defaultExpanded?: boolean
11
13
  }
12
- export default function ValueObject({
14
+ const ValueObject = forwardRef<ParseValueRef, ValueObjectProps>(({
13
15
  value,
14
16
  path,
15
17
  defaultExpanded = false
16
- }: ValueObjectProps) {
18
+ }, ref) => {
17
19
  const debug = useDebugger();
18
20
 
19
21
  const [expanded, setExpanded] = useState(defaultExpanded);
20
22
 
21
23
  const objectEntries = entries(value);
22
24
 
25
+ useImperativeHandle(ref, () => {
26
+ let copyValue: Optional<string> = undefined;
27
+ try
28
+ {
29
+ copyValue = JSON.stringify(value);
30
+ } catch {}
31
+
32
+ return {
33
+ name: `Object {${objectEntries.length}}`,
34
+ copyValue: copyValue,
35
+ menu: [
36
+ {
37
+ id: "open-toggle",
38
+ label: expanded ? "Collapse" : "Expand",
39
+ onClick: () => {
40
+ setExpanded(!expanded);
41
+ }
42
+ }
43
+ ]
44
+ };
45
+ }, [value, expanded]);
46
+
23
47
  const collapsedPreview = useMemo(() => {
24
48
  const children: React.ReactNode[] = [];
25
49
  const keyList = keys(value);
@@ -48,6 +72,8 @@ export default function ValueObject({
48
72
  setExpanded(state);
49
73
  }
50
74
 
75
+ const ctxmListener = useCTXMListener();
76
+
51
77
  return (
52
78
  <>
53
79
  <Chevron_Toggle expanded={expanded} onToggle={handleExpand} />
@@ -65,16 +91,13 @@ export default function ValueObject({
65
91
  {
66
92
  objectEntries.map(([key, value], ix) => {
67
93
  return (
68
- <li key={ix}>
69
- <ValueObjectKey text={key} />
70
- <Char_Colon />
71
-
72
- <ParseValue value={value} path={[...path, `${key}`]} />
73
-
74
- {ix < objectEntries.length - 1 && (
75
- <Char_Comma />
76
- )}
77
- </li>
94
+ <ValueObjectEntry
95
+ key={ix}
96
+ entryKey={key}
97
+ value={value}
98
+ path={path}
99
+ isLast={ix === objectEntries.length - 1}
100
+ />
78
101
  );
79
102
  })
80
103
  }
@@ -97,7 +120,7 @@ export default function ValueObject({
97
120
  }
98
121
  </>
99
122
  );
100
- }
123
+ });
101
124
 
102
125
  interface ValueObjectKeyProps {
103
126
  text: string,
@@ -110,4 +133,33 @@ function ValueObjectKey({
110
133
  {text}
111
134
  </span>
112
135
  )
113
- }
136
+ }
137
+
138
+ interface ValueObjectEntryProps {
139
+ entryKey: string,
140
+ value: unknown,
141
+ path: string[],
142
+ isLast: boolean
143
+ }
144
+ function ValueObjectEntry({
145
+ entryKey: key,
146
+ value,
147
+ path,
148
+ isLast
149
+ }: ValueObjectEntryProps)
150
+ {
151
+ return (
152
+ <li>
153
+ <ParseValue value={value} path={[...path, `${key}`]}>
154
+ <ValueObjectKey text={key} />
155
+ <Char_Colon />
156
+ </ParseValue>
157
+
158
+ {!isLast && (
159
+ <Char_Comma />
160
+ )}
161
+ </li>
162
+ )
163
+ }
164
+
165
+ export default ValueObject;
@@ -1,16 +1,25 @@
1
1
  import { padString } from "@hrnec06/util"
2
+ import { forwardRef, useImperativeHandle } from "react"
3
+ import { ParseValueRef } from "./DebugParser";
2
4
 
3
5
  interface ValueStringProps {
4
6
  value: string,
5
7
  noEncase?: boolean
6
8
  }
7
- export default function ValueString({
9
+ const ValueString = forwardRef<ParseValueRef, ValueStringProps>(({
8
10
  value,
9
11
  noEncase = false
10
- }: ValueStringProps) {
12
+ }, ref) => {
13
+ useImperativeHandle(ref, () => ({
14
+ name: "String",
15
+ copyValue: value
16
+ }), [value]);
17
+
11
18
  return (
12
19
  <span className="text-[#ce9178]">
13
20
  {padString(value, noEncase ? 0 : 1, '"')}
14
21
  </span>
15
- )
16
- }
22
+ );
23
+ });
24
+
25
+ export default ValueString;
@@ -3,7 +3,7 @@ import clsx from "clsx";
3
3
  import { useEffect } from "react";
4
4
  import DragAreas from "./DragAreas";
5
5
  import useSignal from "../../hooks/useSignal";
6
- import useUpdatedRef from "../../hooks/useUpdatedRef";
6
+ import useUpdatedRef from "../../hooks/useLatestRef";
7
7
  import useListener from "../../hooks/useListener";
8
8
  import { ResizeableBoxContext } from "./ResizeableBoxContext";
9
9
  import RB from "./ResizeableBox.types";
@@ -0,0 +1,26 @@
1
+ import { useCallback, useLayoutEffect, useState } from "react";
2
+
3
+ export default function useDebounce<T>(value: T, delay: number, updateOnUnmount: boolean = false): T
4
+ {
5
+ const [debounced, setValue] = useState(value);
6
+
7
+ const update = useCallback((value: T) => setValue(value), []);
8
+
9
+ useLayoutEffect(() => {
10
+ let updated = false;
11
+
12
+ const t = setTimeout(() => {
13
+ update(value);
14
+ updated = true;
15
+ }, delay);
16
+
17
+ return () => {
18
+ clearTimeout(t);
19
+
20
+ if (!updated && updateOnUnmount)
21
+ update(value);
22
+ }
23
+ }, [value]);
24
+
25
+ return debounced;
26
+ }
@@ -0,0 +1,15 @@
1
+ import { useCallback } from "react";
2
+ import useLatestRef from "./useLatestRef";
3
+
4
+ export default function useEvent<
5
+ F extends (...args: any[]) => any,
6
+ P extends any[] = Parameters<F>,
7
+ R = ReturnType<F>,
8
+ > (
9
+ cb: (...args: P) => R
10
+ )
11
+ {
12
+ const cache = useLatestRef(cb);
13
+
14
+ return useCallback((...args: P) => cache.current(...args), [cache]);
15
+ }
@@ -0,0 +1,12 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+ import useUpdateEffect from "./useUpdateEffect";
3
+
4
+ export default function useLatestRef<V>(value: V): React.RefObject<V> {
5
+ const ref = useRef(value);
6
+
7
+ useLayoutEffect(() => {
8
+ ref.current = value;
9
+ });
10
+
11
+ return ref;
12
+ }
@@ -0,0 +1,17 @@
1
+ import { Nullable } from '@hrnec06/util'
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
3
+ import useEvent from './useEvent';
4
+
5
+ export default function useSyncRef<T>(
6
+ ref: React.RefObject<T>,
7
+ state: (value: T) => void
8
+ )
9
+ {
10
+ const syncRefs = useEvent((value: T) => {
11
+ ref.current = value;
12
+
13
+ state(value);
14
+ });
15
+
16
+ return syncRefs;
17
+ }