@dxos/react-hooks 0.8.3 → 0.8.4-main.1068cf700f
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.
- package/dist/lib/browser/index.mjs +232 -137
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +232 -137
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/index.d.ts +5 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/useAsyncEffect.d.ts +5 -29
- package/dist/types/src/useAsyncEffect.d.ts.map +1 -1
- package/dist/types/src/useAsyncState.d.ts.map +1 -1
- package/dist/types/src/useControlledState.d.ts +2 -1
- package/dist/types/src/useControlledState.d.ts.map +1 -1
- package/dist/types/src/useDebugDeps.d.ts +6 -0
- package/dist/types/src/useDebugDeps.d.ts.map +1 -0
- package/dist/types/src/useDefaultValue.d.ts.map +1 -1
- package/dist/types/src/useDefaults.d.ts +6 -0
- package/dist/types/src/useDefaults.d.ts.map +1 -0
- package/dist/types/src/useDynamicRef.d.ts +6 -1
- package/dist/types/src/useDynamicRef.d.ts.map +1 -1
- package/dist/types/src/useForwardedRef.d.ts +23 -3
- package/dist/types/src/useForwardedRef.d.ts.map +1 -1
- package/dist/types/src/useId.d.ts.map +1 -1
- package/dist/types/src/useIsFocused.d.ts +1 -1
- package/dist/types/src/useIsFocused.d.ts.map +1 -1
- package/dist/types/src/useMediaQuery.d.ts +1 -1
- package/dist/types/src/useMediaQuery.d.ts.map +1 -1
- package/dist/types/src/useMulticastObservable.test.d.ts.map +1 -1
- package/dist/types/src/useTimeout.d.ts +3 -0
- package/dist/types/src/useTimeout.d.ts.map +1 -0
- package/dist/types/src/useTransitions.d.ts +2 -1
- package/dist/types/src/useTransitions.d.ts.map +1 -1
- package/dist/types/src/useViewportResize.d.ts +3 -0
- package/dist/types/src/useViewportResize.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -11
- package/src/index.ts +6 -3
- package/src/useAsyncEffect.ts +19 -49
- package/src/useAsyncState.ts +2 -1
- package/src/useControlledState.ts +26 -7
- package/src/useDebugDeps.ts +35 -0
- package/src/useDefaultValue.ts +1 -2
- package/src/useDefaults.ts +14 -0
- package/src/useDynamicRef.ts +26 -5
- package/src/useForwardedRef.ts +48 -13
- package/src/useId.ts +3 -2
- package/src/useIsFocused.ts +2 -2
- package/src/useMediaQuery.ts +8 -9
- package/src/{useMulticastObservable.test.tsx → useMulticastObservable.test.ts} +1 -3
- package/src/useTimeout.ts +46 -0
- package/src/useTransitions.ts +4 -2
- package/src/{useResize.ts → useViewportResize.ts} +4 -4
- package/dist/lib/node/index.cjs +0 -413
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/types/src/useAsyncEffect.test.d.ts +0 -2
- package/dist/types/src/useAsyncEffect.test.d.ts.map +0 -1
- package/dist/types/src/useDebugReactDeps.d.ts +0 -6
- package/dist/types/src/useDebugReactDeps.d.ts.map +0 -1
- package/dist/types/src/useResize.d.ts +0 -3
- package/dist/types/src/useResize.d.ts.map +0 -1
- package/dist/types/src/useTrackProps.d.ts +0 -5
- package/dist/types/src/useTrackProps.d.ts.map +0 -1
- package/src/useAsyncEffect.test.tsx +0 -51
- package/src/useDebugReactDeps.ts +0 -27
- package/src/useTrackProps.ts +0 -40
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-hooks",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4-main.1068cf700f",
|
|
4
4
|
"description": "React hooks supporting DXOS React primitives.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
13
|
+
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
17
|
+
"source": "./src/index.ts",
|
|
13
18
|
"types": "./dist/types/src/index.d.ts",
|
|
14
19
|
"browser": "./dist/lib/browser/index.mjs",
|
|
15
20
|
"node": "./dist/lib/node-esm/index.mjs"
|
|
@@ -24,20 +29,23 @@
|
|
|
24
29
|
"src"
|
|
25
30
|
],
|
|
26
31
|
"dependencies": {
|
|
27
|
-
"@
|
|
32
|
+
"@radix-ui/react-id": "1.1.0",
|
|
28
33
|
"alea": "^1.0.1",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
34
|
+
"lodash.defaultsdeep": "^4.6.1",
|
|
35
|
+
"mini-virtual-list": "^0.3.2",
|
|
36
|
+
"@dxos/async": "0.8.4-main.1068cf700f",
|
|
37
|
+
"@dxos/log": "0.8.4-main.1068cf700f"
|
|
31
38
|
},
|
|
32
39
|
"devDependencies": {
|
|
33
|
-
"@types/
|
|
34
|
-
"@types/react
|
|
35
|
-
"react": "~
|
|
36
|
-
"react
|
|
40
|
+
"@types/lodash.defaultsdeep": "^4.6.6",
|
|
41
|
+
"@types/react": "~19.2.7",
|
|
42
|
+
"@types/react-dom": "~19.2.3",
|
|
43
|
+
"react": "~19.2.3",
|
|
44
|
+
"react-dom": "~19.2.3"
|
|
37
45
|
},
|
|
38
46
|
"peerDependencies": {
|
|
39
|
-
"react": "~
|
|
40
|
-
"react-dom": "~
|
|
47
|
+
"react": "~19.2.3",
|
|
48
|
+
"react-dom": "~19.2.3"
|
|
41
49
|
},
|
|
42
50
|
"publishConfig": {
|
|
43
51
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
export * from './useAsyncEffect';
|
|
6
6
|
export * from './useAsyncState';
|
|
7
7
|
export * from './useControlledState';
|
|
8
|
-
export * from './
|
|
8
|
+
export * from './useDebugDeps';
|
|
9
9
|
export * from './useDefaultValue';
|
|
10
|
+
export * from './useDefaults';
|
|
10
11
|
export * from './useDynamicRef';
|
|
11
12
|
export * from './useFileDownload';
|
|
12
13
|
export * from './useForwardedRef';
|
|
@@ -15,6 +16,8 @@ export * from './useIsFocused';
|
|
|
15
16
|
export * from './useMediaQuery';
|
|
16
17
|
export * from './useMulticastObservable';
|
|
17
18
|
export * from './useRefCallback';
|
|
18
|
-
export * from './
|
|
19
|
-
export * from './
|
|
19
|
+
export * from './useViewportResize';
|
|
20
|
+
export * from './useTimeout';
|
|
20
21
|
export * from './useTransitions';
|
|
22
|
+
|
|
23
|
+
export { useSize, useScroller } from 'mini-virtual-list';
|
package/src/useAsyncEffect.ts
CHANGED
|
@@ -2,61 +2,31 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useEffect } from 'react';
|
|
6
|
-
|
|
7
|
-
import { log } from '@dxos/log';
|
|
5
|
+
import { type DependencyList, type EffectCallback, useEffect } from 'react';
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* ```tsx
|
|
14
|
-
* useAsyncEffect(async () => {
|
|
15
|
-
* await test();
|
|
16
|
-
* }, []);
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
19
|
-
* The callback may check of the component is still mounted before doing state updates.
|
|
20
|
-
*
|
|
21
|
-
* ```tsx
|
|
22
|
-
* const [value, setValue] = useState<string>();
|
|
23
|
-
* useAsyncEffect<string>(async (isMounted) => {
|
|
24
|
-
* const value = await test();
|
|
25
|
-
* if (!isMounted()) {
|
|
26
|
-
* setValue(value);
|
|
27
|
-
* }
|
|
28
|
-
* }, () => console.log('Unmounted'), []);
|
|
29
|
-
* ```
|
|
30
|
-
*
|
|
31
|
-
* @param callback Receives a getter function that determines if the component is still mounted.
|
|
32
|
-
* @param destructor Receives the value returned from the callback.
|
|
33
|
-
* @param deps
|
|
34
|
-
*
|
|
35
|
-
* NOTE: This effect does not cancel the async operation if the component is unmounted.
|
|
36
|
-
*
|
|
37
|
-
* @deprecated
|
|
8
|
+
* Async version of useEffect.
|
|
9
|
+
* The `AbortController` can be used to detect if the component has been unmounted and
|
|
10
|
+
* can be used to propagate abort signals to downstream async operations (e.g., `fetch`).
|
|
38
11
|
*/
|
|
39
|
-
export const useAsyncEffect =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
deps?: any[],
|
|
12
|
+
export const useAsyncEffect = (
|
|
13
|
+
cb: (controller: AbortController) => Promise<EffectCallback | void>,
|
|
14
|
+
deps?: DependencyList,
|
|
43
15
|
) => {
|
|
44
|
-
const [effectDestructor, effectDeps] =
|
|
45
|
-
typeof destructor === 'function' ? [destructor, deps] : [undefined, destructor];
|
|
46
|
-
|
|
47
16
|
useEffect(() => {
|
|
48
|
-
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
let cleanup: EffectCallback | void;
|
|
19
|
+
// NOTE: Timeout enables us to immediately cancel. if the component is unmounted.
|
|
20
|
+
const t = setTimeout(async () => {
|
|
21
|
+
if (!controller.signal.aborted) {
|
|
22
|
+
cleanup = await cb(controller);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
56
25
|
|
|
57
26
|
return () => {
|
|
58
|
-
|
|
59
|
-
|
|
27
|
+
clearTimeout(t);
|
|
28
|
+
controller.abort();
|
|
29
|
+
cleanup?.();
|
|
60
30
|
};
|
|
61
|
-
},
|
|
31
|
+
}, deps ?? []);
|
|
62
32
|
};
|
package/src/useAsyncState.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const useAsyncState = <T>(
|
|
|
14
14
|
const [value, setValue] = useState<T | undefined>();
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
let disposed = false;
|
|
17
|
-
|
|
17
|
+
const t = setTimeout(async () => {
|
|
18
18
|
const data = await cb();
|
|
19
19
|
if (!disposed) {
|
|
20
20
|
setValue(data);
|
|
@@ -23,6 +23,7 @@ export const useAsyncState = <T>(
|
|
|
23
23
|
|
|
24
24
|
return () => {
|
|
25
25
|
disposed = true;
|
|
26
|
+
clearTimeout(t);
|
|
26
27
|
};
|
|
27
28
|
}, deps);
|
|
28
29
|
|
|
@@ -2,18 +2,37 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
|
|
5
|
+
import { type Dispatch, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useDynamicRef } from './useDynamicRef';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* A stateful hook with a controlled value.
|
|
11
|
+
* NOTE: Consider using Radix's `useControllableState`.
|
|
9
12
|
*/
|
|
10
|
-
export const useControlledState = <T>(
|
|
11
|
-
|
|
13
|
+
export const useControlledState = <T>(
|
|
14
|
+
valueProp: T,
|
|
15
|
+
onChange?: (value: T) => void,
|
|
16
|
+
): [T, Dispatch<SetStateAction<T>>] => {
|
|
17
|
+
const [value, setControlledValue] = useState(valueProp);
|
|
12
18
|
useEffect(() => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
setControlledValue(valueProp);
|
|
20
|
+
}, [valueProp]);
|
|
21
|
+
|
|
22
|
+
const onChangeRef = useRef(onChange);
|
|
23
|
+
const valueRef = useDynamicRef(valueProp);
|
|
24
|
+
const setValue = useCallback<Dispatch<SetStateAction<T>>>(
|
|
25
|
+
(nextValue) => {
|
|
26
|
+
const value = isFunction(nextValue) ? nextValue(valueRef.current) : nextValue;
|
|
27
|
+
setControlledValue(value);
|
|
28
|
+
onChangeRef.current?.(value);
|
|
29
|
+
},
|
|
30
|
+
[valueRef, onChangeRef],
|
|
31
|
+
);
|
|
17
32
|
|
|
18
33
|
return [value, setValue];
|
|
19
34
|
};
|
|
35
|
+
|
|
36
|
+
function isFunction(value: unknown): value is (...args: any[]) => any {
|
|
37
|
+
return typeof value === 'function';
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type DependencyList, useEffect, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { log } from '@dxos/log';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Util to log deps that have changed.
|
|
11
|
+
*/
|
|
12
|
+
export const useDebugDeps = (deps: DependencyList = [], label = 'useDebugDeps', active = true) => {
|
|
13
|
+
const lastDeps = useRef<DependencyList>([]);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!active) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const diff: Record<number, { previous: any; current: any }> = {};
|
|
20
|
+
for (let i = 0; i < Math.max(lastDeps.current.length ?? 0, deps.length ?? 0); i++) {
|
|
21
|
+
if (lastDeps.current[i] !== deps[i] || i > lastDeps.current.length) {
|
|
22
|
+
diff[i] = {
|
|
23
|
+
previous: lastDeps.current[i],
|
|
24
|
+
current: deps[i],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (Object.keys(diff).length > 0) {
|
|
30
|
+
log.warn(`Updated: ${label} [${lastDeps.current.length}/${deps.length}]`, diff);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
lastDeps.current = deps;
|
|
34
|
+
}, [...deps, active]);
|
|
35
|
+
};
|
package/src/useDefaultValue.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useEffect,
|
|
5
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* A custom React hook that provides a stable default value for a potentially undefined reactive value.
|
|
@@ -21,7 +21,6 @@ export const useDefaultValue = <T>(reactiveValue: T | undefined | null, getDefau
|
|
|
21
21
|
// regardless of whether the defaultValue changes.
|
|
22
22
|
const stableDefaultValue = useMemo(getDefaultValue, []);
|
|
23
23
|
const [value, setValue] = useState(reactiveValue ?? stableDefaultValue);
|
|
24
|
-
|
|
25
24
|
useEffect(() => {
|
|
26
25
|
setValue(reactiveValue ?? stableDefaultValue);
|
|
27
26
|
}, [reactiveValue, stableDefaultValue]);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns a memo-ized deep-merged object of the default and value.
|
|
10
|
+
* If value is undefined or null, then returns the default.
|
|
11
|
+
*/
|
|
12
|
+
export const useDefaults = <T>(value: T | undefined | null, defaults: T): T => {
|
|
13
|
+
return useMemo(() => defaultsDeep({}, defaults, value), [value, defaults]);
|
|
14
|
+
};
|
package/src/useDynamicRef.ts
CHANGED
|
@@ -2,16 +2,37 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useEffect, useRef } from 'react';
|
|
5
|
+
import { type Dispatch, type RefObject, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Like `useState` but with an additional dynamic value.
|
|
9
|
+
*/
|
|
10
|
+
export const useStateWithRef = <T>(valueProp: T): [T, Dispatch<SetStateAction<T>>, RefObject<T>] => {
|
|
11
|
+
const [value, setValue] = useState<T>(valueProp);
|
|
12
|
+
const valueRef = useRef<T>(valueProp);
|
|
13
|
+
const setter = useCallback<Dispatch<SetStateAction<T>>>((value) => {
|
|
14
|
+
if (typeof value === 'function') {
|
|
15
|
+
setValue((current) => {
|
|
16
|
+
valueRef.current = (value as Function)(current);
|
|
17
|
+
return valueRef.current;
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
valueRef.current = value;
|
|
21
|
+
setValue(value);
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
return [value, setter, valueRef];
|
|
26
|
+
};
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
29
|
* Ref that is updated by a dependency.
|
|
9
30
|
*/
|
|
10
|
-
export const useDynamicRef = <T>(value: T) => {
|
|
11
|
-
const
|
|
31
|
+
export const useDynamicRef = <T>(value: T): RefObject<T> => {
|
|
32
|
+
const valueRef = useRef<T>(value);
|
|
12
33
|
useEffect(() => {
|
|
13
|
-
|
|
34
|
+
valueRef.current = value;
|
|
14
35
|
}, [value]);
|
|
15
36
|
|
|
16
|
-
return
|
|
37
|
+
return valueRef;
|
|
17
38
|
};
|
package/src/useForwardedRef.ts
CHANGED
|
@@ -2,25 +2,60 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type ForwardedRef,
|
|
5
|
+
import { type ForwardedRef, type Ref, type RefCallback, useEffect, useMemo, useRef } from 'react';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Combines a possibly undefined forwarded ref with a locally defined ref.
|
|
9
|
-
*
|
|
9
|
+
* Returns a stable ref object that synchronizes with the forwarded ref.
|
|
10
|
+
*
|
|
11
|
+
* Best practice: This hook creates a stable local ref and synchronizes it with the forwarded ref.
|
|
12
|
+
* The returned ref object is stable across renders, preventing infinite loops caused by ref identity changes.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: This pattern doesn't update refs once they are set. If this is required, use `useMergeRefs`.
|
|
10
15
|
*/
|
|
11
|
-
export const useForwardedRef = <T>(
|
|
12
|
-
const
|
|
16
|
+
export const useForwardedRef = <T>(forwardedRef: ForwardedRef<T>) => {
|
|
17
|
+
const localRef = useRef<T>(null as T);
|
|
13
18
|
useEffect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
setRef(forwardedRef, localRef.current);
|
|
20
|
+
}, [forwardedRef]);
|
|
21
|
+
|
|
22
|
+
return localRef;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets a value on a React ref, handling both callback refs and ref objects.
|
|
27
|
+
* Returns a cleanup function if the ref is a callback ref.
|
|
28
|
+
*/
|
|
29
|
+
export function setRef<T>(ref: Ref<T> | undefined | null, value: T | null): ReturnType<RefCallback<T>> {
|
|
30
|
+
if (typeof ref === 'function') {
|
|
31
|
+
return ref(value);
|
|
32
|
+
} else if (ref) {
|
|
33
|
+
ref.current = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
17
36
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Merges multiple refs into a single ref callback.
|
|
39
|
+
* Returns a ref callback that synchronizes all provided refs and handles cleanup.
|
|
40
|
+
*/
|
|
41
|
+
export const mergeRefs = <T>(refs: (Ref<T> | undefined)[]): Ref<T> => {
|
|
42
|
+
return (value: T | null) => {
|
|
43
|
+
const cleanups: (() => void)[] = [];
|
|
44
|
+
for (const ref of refs) {
|
|
45
|
+
const cleanup = setRef(ref, value);
|
|
46
|
+
cleanups.push(typeof cleanup === 'function' ? cleanup : () => setRef(ref, null));
|
|
22
47
|
}
|
|
23
|
-
});
|
|
24
48
|
|
|
25
|
-
|
|
49
|
+
return () => {
|
|
50
|
+
for (const cleanup of cleanups) cleanup();
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hook that merges multiple refs into a single stable ref callback.
|
|
57
|
+
* The returned ref is memoized and only changes when the refs array changes.
|
|
58
|
+
*/
|
|
59
|
+
export const useMergeRefs = <T>(refs: (Ref<T> | undefined)[]): Ref<T> => {
|
|
60
|
+
return useMemo(() => mergeRefs(refs), [...refs]);
|
|
26
61
|
};
|
package/src/useId.ts
CHANGED
|
@@ -19,8 +19,9 @@ export const randomString = (n = 4) =>
|
|
|
19
19
|
.toString(16)
|
|
20
20
|
.slice(2, n + 2);
|
|
21
21
|
|
|
22
|
-
export const useId = (namespace: string, propsId?: string, opts?: Partial<{ n: number }>) =>
|
|
23
|
-
useMemo(() => makeId(namespace, propsId, opts), [propsId]);
|
|
22
|
+
export const useId = (namespace: string, propsId?: string, opts?: Partial<{ n: number }>) => {
|
|
23
|
+
return useMemo(() => makeId(namespace, propsId, opts), [propsId]);
|
|
24
|
+
};
|
|
24
25
|
|
|
25
26
|
export const makeId = (namespace: string, propsId?: string, opts?: Partial<{ n: number }>) =>
|
|
26
27
|
propsId ?? `${namespace}-${randomString(opts?.n ?? 4)}`;
|
package/src/useIsFocused.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// Based upon the useIsFocused hook which is part of the `rci` project:
|
|
6
6
|
/// https://github.com/leonardodino/rci/blob/main/packages/use-is-focused
|
|
7
7
|
|
|
8
|
-
import { useEffect, useRef, useState
|
|
8
|
+
import { type RefObject, useEffect, useRef, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
export const useIsFocused = (inputRef: RefObject<HTMLInputElement>) => {
|
|
10
|
+
export const useIsFocused = (inputRef: RefObject<HTMLInputElement | null>) => {
|
|
11
11
|
const [isFocused, setIsFocused] = useState<boolean | undefined>(undefined);
|
|
12
12
|
const isFocusedRef = useRef<boolean | undefined>(isFocused);
|
|
13
13
|
|
package/src/useMediaQuery.ts
CHANGED
|
@@ -6,12 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
fallback?: boolean | boolean[];
|
|
11
|
-
ssr?: boolean;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// TODO(thure): This should be derived from the same source of truth as the Tailwind theme config
|
|
9
|
+
// TODO(thure): This should be derived from the same source of truth as the Tailwind theme config.
|
|
15
10
|
const breakpointMediaQueries: Record<string, string> = {
|
|
16
11
|
sm: '(min-width: 640px)',
|
|
17
12
|
md: '(min-width: 768px)',
|
|
@@ -20,8 +15,13 @@ const breakpointMediaQueries: Record<string, string> = {
|
|
|
20
15
|
'2xl': '(min-width: 1536px)',
|
|
21
16
|
};
|
|
22
17
|
|
|
18
|
+
export type UseMediaQueryOptions = {
|
|
19
|
+
fallback?: boolean | boolean[];
|
|
20
|
+
ssr?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
23
|
/**
|
|
24
|
-
* React hook that tracks state of a CSS media query
|
|
24
|
+
* React hook that tracks state of a CSS media query.
|
|
25
25
|
*
|
|
26
26
|
* @param query the media query to match, or a recognized breakpoint token
|
|
27
27
|
* @param options the media query options { fallback, ssr }
|
|
@@ -29,8 +29,7 @@ const breakpointMediaQueries: Record<string, string> = {
|
|
|
29
29
|
* @see Docs https://chakra-ui.com/docs/hooks/use-media-query
|
|
30
30
|
*/
|
|
31
31
|
export const useMediaQuery = (query: string | string[], options: UseMediaQueryOptions = {}): boolean[] => {
|
|
32
|
-
|
|
33
|
-
const { ssr = true, fallback } = options;
|
|
32
|
+
const { ssr = false, fallback } = options;
|
|
34
33
|
|
|
35
34
|
const queries = (Array.isArray(query) ? query : [query]).map((query) =>
|
|
36
35
|
query in breakpointMediaQueries ? breakpointMediaQueries[query] : query,
|
|
@@ -15,9 +15,7 @@ describe('useMulticastObservable', () => {
|
|
|
15
15
|
const observable = MulticastObservable.from(event, 0);
|
|
16
16
|
const { result } = renderHook(() => useMulticastObservable(observable));
|
|
17
17
|
expect(result.current).toEqual(0);
|
|
18
|
-
|
|
19
|
-
await act(() => event.emit(1));
|
|
20
|
-
|
|
18
|
+
await act(async () => event.emit(1));
|
|
21
19
|
await expect.poll(() => result.current).toEqual(1);
|
|
22
20
|
});
|
|
23
21
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
export const useTimeout = (callback?: () => Promise<void>, delay = 0, deps: any[] = []) => {
|
|
8
|
+
const callbackRef = useRef(callback);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
callbackRef.current = callback;
|
|
11
|
+
}, [callback]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (delay == null) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const t = setTimeout(() => callbackRef.current?.(), delay);
|
|
19
|
+
return () => clearTimeout(t);
|
|
20
|
+
}, [delay, ...deps]);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const useInterval = (
|
|
24
|
+
callback?: (() => Promise<void | boolean>) | (() => void | boolean),
|
|
25
|
+
delay = 0,
|
|
26
|
+
deps: any[] = [],
|
|
27
|
+
) => {
|
|
28
|
+
const callbackRef = useRef(callback);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
callbackRef.current = callback;
|
|
31
|
+
}, [callback]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (delay == null) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const i = setInterval(async () => {
|
|
39
|
+
const result = await callbackRef.current?.();
|
|
40
|
+
if (result === false) {
|
|
41
|
+
clearInterval(i);
|
|
42
|
+
}
|
|
43
|
+
}, delay);
|
|
44
|
+
return () => clearInterval(i);
|
|
45
|
+
}, [delay, ...deps]);
|
|
46
|
+
};
|
package/src/useTransitions.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useEffect, useRef, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
const isFunction = <T>(functionToCheck: any): functionToCheck is (value: T) => boolean => {
|
|
8
8
|
return functionToCheck instanceof Function;
|
|
@@ -48,7 +48,9 @@ export const useDidTransition = <T>(
|
|
|
48
48
|
* Executes a callback function when a specified transition occurs in a value.
|
|
49
49
|
*
|
|
50
50
|
* This function utilizes the `useDidTransition` hook to monitor changes in `currentValue`.
|
|
51
|
-
* When `currentValue` transitions from `fromValue` to `toValue`, the provided `callback` function is executed.
|
|
51
|
+
* When `currentValue` transitions from `fromValue` to `toValue`, the provided `callback` function is executed.
|
|
52
|
+
*/
|
|
53
|
+
// TODO(wittjosiah): Seems overwrought.
|
|
52
54
|
export const useOnTransition = <T>(
|
|
53
55
|
currentValue: T,
|
|
54
56
|
fromValue: T | ((value: T) => boolean),
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { useLayoutEffect, useMemo } from 'react';
|
|
6
6
|
|
|
7
|
-
export const
|
|
8
|
-
|
|
7
|
+
export const useViewportResize = (
|
|
8
|
+
cb: (event?: Event) => void,
|
|
9
9
|
deps: Parameters<typeof useLayoutEffect>[1] = [],
|
|
10
10
|
delay: number = 800,
|
|
11
11
|
) => {
|
|
@@ -14,10 +14,10 @@ export const useResize = (
|
|
|
14
14
|
return (event?: Event) => {
|
|
15
15
|
clearTimeout(timeout);
|
|
16
16
|
timeout = setTimeout(() => {
|
|
17
|
-
|
|
17
|
+
cb(event);
|
|
18
18
|
}, delay);
|
|
19
19
|
};
|
|
20
|
-
}, [
|
|
20
|
+
}, [cb, delay]);
|
|
21
21
|
|
|
22
22
|
return useLayoutEffect(() => {
|
|
23
23
|
window.visualViewport?.addEventListener('resize', debouncedHandler);
|