@dxos/react-hooks 0.8.4-main.5ad4a44 → 0.8.4-main.66e292d

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-hooks",
3
- "version": "0.8.4-main.5ad4a44",
3
+ "version": "0.8.4-main.66e292d",
4
4
  "description": "React hooks supporting DXOS React primitives.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -26,15 +26,17 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "@preact-signals/safe-react": "^0.9.0",
29
+ "@radix-ui/react-use-controllable-state": "^1.2.2",
29
30
  "alea": "^1.0.1",
30
31
  "lodash.defaultsdeep": "^4.6.1",
31
- "@dxos/async": "0.8.4-main.5ad4a44",
32
- "@dxos/log": "0.8.4-main.5ad4a44"
32
+ "mini-virtual-list": "^0.3.2",
33
+ "@dxos/async": "0.8.4-main.66e292d",
34
+ "@dxos/log": "0.8.4-main.66e292d"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@types/lodash.defaultsdeep": "^4.6.6",
36
38
  "@types/react": "~19.2.2",
37
- "@types/react-dom": "~19.2.1",
39
+ "@types/react-dom": "~19.2.2",
38
40
  "react": "~19.2.0",
39
41
  "react-dom": "~19.2.0"
40
42
  },
package/src/index.ts CHANGED
@@ -16,7 +16,9 @@ export * from './useIsFocused';
16
16
  export * from './useMediaQuery';
17
17
  export * from './useMulticastObservable';
18
18
  export * from './useRefCallback';
19
- export * from './useResize';
19
+ export * from './useViewportResize';
20
20
  export * from './useSignals';
21
21
  export * from './useTimeout';
22
22
  export * from './useTransitions';
23
+
24
+ export { useSize, useScroller } from 'mini-virtual-list';
@@ -2,27 +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.
9
- * NOTE: Be careful not to provide an inlinde default array.
11
+ * @deprecated Use Radix `useControllableState` (NOTE: `useControlledState` is not compatible with `useControllableState`)
10
12
  */
11
13
  export const useControlledState = <T>(
12
- controlledValue: T,
14
+ valueParam: T,
13
15
  onChange?: (value: T) => void,
14
- ...deps: any[]
15
16
  ): [T, Dispatch<SetStateAction<T>>] => {
16
- const [value, setValue] = useState<T>(controlledValue);
17
+ const [value, setControlledValue] = useState(valueParam);
17
18
  useEffect(() => {
18
- if (controlledValue !== undefined) {
19
- setValue(controlledValue);
20
- }
21
- }, [controlledValue, ...deps]);
19
+ setControlledValue(valueParam);
20
+ }, [valueParam]);
22
21
 
23
- useEffect(() => {
24
- onChange?.(value);
25
- }, [value, onChange]);
22
+ const onChangeRef = useRef(onChange);
23
+ const valueRef = useDynamicRef(valueParam);
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
+ );
26
32
 
27
33
  return [value, setValue];
28
34
  };
35
+
36
+ function isFunction(value: unknown): value is (...args: any[]) => any {
37
+ return typeof value === 'function';
38
+ }
@@ -2,13 +2,12 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback } from '@preact-signals/safe-react/react';
6
- import { type Dispatch, type MutableRefObject, type SetStateAction, useEffect, useRef, useState } from 'react';
5
+ import { type Dispatch, type RefObject, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
7
6
 
8
7
  /**
9
8
  * Like `useState` but with an additional dynamic value.
10
9
  */
11
- export const useStateWithRef = <T>(valueParam: T): [T, Dispatch<SetStateAction<T>>, MutableRefObject<T>] => {
10
+ export const useStateWithRef = <T>(valueParam: T): [T, Dispatch<SetStateAction<T>>, RefObject<T>] => {
12
11
  const [value, setValue] = useState<T>(valueParam);
13
12
  const valueRef = useRef<T>(valueParam);
14
13
  const setter = useCallback<Dispatch<SetStateAction<T>>>((value) => {
@@ -29,7 +28,7 @@ export const useStateWithRef = <T>(valueParam: T): [T, Dispatch<SetStateAction<T
29
28
  /**
30
29
  * Ref that is updated by a dependency.
31
30
  */
32
- export const useDynamicRef = <T>(value: T): MutableRefObject<T> => {
31
+ export const useDynamicRef = <T>(value: T): RefObject<T> => {
33
32
  const valueRef = useRef<T>(value);
34
33
  useEffect(() => {
35
34
  valueRef.current = value;
@@ -2,25 +2,60 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { type ForwardedRef, useEffect, useRef } from 'react';
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
- * @deprecated Use `useComposedRefs` from @radix-ui/react-compose-refs
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>(ref: ForwardedRef<T>) => {
12
- const innerRef = useRef<T>(null);
16
+ export const useForwardedRef = <T>(forwardedRef: ForwardedRef<T>) => {
17
+ const localRef = useRef<T>(null as T);
13
18
  useEffect(() => {
14
- if (!ref) {
15
- return;
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
- if (typeof ref === 'function') {
19
- ref(innerRef.current);
20
- } else {
21
- ref.current = innerRef.current;
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
- return innerRef;
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
  };
@@ -6,12 +6,7 @@
6
6
 
7
7
  import { useEffect, useState } from 'react';
8
8
 
9
- export type UseMediaQueryOptions = {
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 }
@@ -15,7 +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
- act(() => event.emit(1));
18
+ await act(async () => event.emit(1));
19
19
  await expect.poll(() => result.current).toEqual(1);
20
20
  });
21
21
  });
package/src/useSignals.ts CHANGED
@@ -3,8 +3,7 @@
3
3
  //
4
4
 
5
5
  import { computed, effect } from '@preact-signals/safe-react';
6
- import { useRef } from '@preact-signals/safe-react/react';
7
- import { type DependencyList, useEffect, useMemo } from 'react';
6
+ import { type DependencyList, useEffect, useMemo, useRef } from 'react';
8
7
 
9
8
  /**
10
9
  * Like `useEffect` but also tracks signals inside of the callback.
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useLayoutEffect, useMemo } from 'react';
6
6
 
7
- export const useResize = (
7
+ export const useViewportResize = (
8
8
  handler: (event?: Event) => void,
9
9
  deps: Parameters<typeof useLayoutEffect>[1] = [],
10
10
  delay: number = 800,
@@ -1,3 +0,0 @@
1
- import { useLayoutEffect } from 'react';
2
- export declare const useResize: (handler: (event?: Event) => void, deps?: Parameters<typeof useLayoutEffect>[1], delay?: number) => void;
3
- //# sourceMappingURL=useResize.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useResize.d.ts","sourceRoot":"","sources":["../../../src/useResize.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,eAAe,EAAW,MAAM,OAAO,CAAC;AAEjD,eAAO,MAAM,SAAS,GACpB,SAAS,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,EAChC,OAAM,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAM,EAChD,QAAO,MAAY,SAiBpB,CAAC"}