@dxos/react-hooks 0.8.4-main.fffef41 → 0.8.4-staging.60fe92afc8

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 (46) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -0
  3. package/dist/lib/browser/index.mjs +122 -98
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/node-esm/index.mjs +122 -98
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/types/src/index.d.ts +3 -2
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/useAsyncEffect.d.ts.map +1 -1
  12. package/dist/types/src/useAsyncState.d.ts.map +1 -1
  13. package/dist/types/src/useAtomState.d.ts +12 -0
  14. package/dist/types/src/useAtomState.d.ts.map +1 -0
  15. package/dist/types/src/useControlledState.d.ts +2 -2
  16. package/dist/types/src/useControlledState.d.ts.map +1 -1
  17. package/dist/types/src/useDebugDeps.d.ts +1 -1
  18. package/dist/types/src/useDebugDeps.d.ts.map +1 -1
  19. package/dist/types/src/useDefaultValue.d.ts.map +1 -1
  20. package/dist/types/src/useDefaults.d.ts.map +1 -1
  21. package/dist/types/src/useDynamicRef.d.ts +1 -1
  22. package/dist/types/src/useDynamicRef.d.ts.map +1 -1
  23. package/dist/types/src/useForwardedRef.d.ts.map +1 -1
  24. package/dist/types/src/useId.d.ts.map +1 -1
  25. package/dist/types/src/useIsFocused.d.ts.map +1 -1
  26. package/dist/types/src/useMediaQuery.d.ts.map +1 -1
  27. package/dist/types/src/useMulticastObservable.d.ts.map +1 -1
  28. package/dist/types/src/useTimeout.d.ts.map +1 -1
  29. package/dist/types/src/useTransitions.d.ts.map +1 -1
  30. package/dist/types/src/useViewportResize.d.ts +1 -1
  31. package/dist/types/src/useViewportResize.d.ts.map +1 -1
  32. package/dist/types/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +18 -16
  34. package/src/index.ts +4 -3
  35. package/src/useAtomState.ts +23 -0
  36. package/src/useControlledState.ts +6 -6
  37. package/src/useDebugDeps.ts +17 -8
  38. package/src/useDynamicRef.ts +4 -5
  39. package/src/useForwardedRef.ts +4 -2
  40. package/src/useId.ts +3 -2
  41. package/src/useIsFocused.ts +1 -1
  42. package/src/useMulticastObservable.test.ts +1 -1
  43. package/src/useViewportResize.ts +36 -11
  44. package/dist/types/src/useSignals.d.ts +0 -10
  45. package/dist/types/src/useSignals.d.ts.map +0 -1
  46. package/src/useSignals.ts +0 -27
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@dxos/react-hooks",
3
- "version": "0.8.4-main.fffef41",
3
+ "version": "0.8.4-staging.60fe92afc8",
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
- "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "FSL-1.1-Apache-2.0",
8
12
  "author": "DXOS.org",
9
- "sideEffects": true,
13
+ "sideEffects": false,
10
14
  "type": "module",
11
15
  "exports": {
12
16
  ".": {
@@ -17,32 +21,30 @@
17
21
  }
18
22
  },
19
23
  "types": "dist/types/src/index.d.ts",
20
- "typesVersions": {
21
- "*": {}
22
- },
23
24
  "files": [
24
25
  "dist",
25
26
  "src"
26
27
  ],
27
28
  "dependencies": {
28
- "@preact-signals/safe-react": "^0.9.0",
29
- "@radix-ui/react-use-controllable-state": "^1.2.2",
29
+ "@effect-atom/atom-react": "^0.5.0",
30
+ "@radix-ui/react-compose-refs": "1.1.1",
31
+ "@radix-ui/react-id": "1.1.0",
30
32
  "alea": "^1.0.1",
31
33
  "lodash.defaultsdeep": "^4.6.1",
32
34
  "mini-virtual-list": "^0.3.2",
33
- "@dxos/async": "0.8.4-main.fffef41",
34
- "@dxos/log": "0.8.4-main.fffef41"
35
+ "@dxos/async": "0.8.4-staging.60fe92afc8",
36
+ "@dxos/log": "0.8.4-staging.60fe92afc8"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@types/lodash.defaultsdeep": "^4.6.6",
38
- "@types/react": "~19.2.2",
39
- "@types/react-dom": "~19.2.2",
40
- "react": "~19.2.0",
41
- "react-dom": "~19.2.0"
40
+ "@types/react": "~19.2.7",
41
+ "@types/react-dom": "~19.2.3",
42
+ "react": "~19.2.3",
43
+ "react-dom": "~19.2.3"
42
44
  },
43
45
  "peerDependencies": {
44
- "react": "^19.0.0",
45
- "react-dom": "^19.0.0"
46
+ "react": "~19.2.3",
47
+ "react-dom": "~19.2.3"
46
48
  },
47
49
  "publishConfig": {
48
50
  "access": "public"
package/src/index.ts CHANGED
@@ -2,7 +2,11 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ export { useComposedRefs } from '@radix-ui/react-compose-refs';
6
+ export { useSize, useScroller } from 'mini-virtual-list';
7
+
5
8
  export * from './useAsyncEffect';
9
+ export * from './useAtomState';
6
10
  export * from './useAsyncState';
7
11
  export * from './useControlledState';
8
12
  export * from './useDebugDeps';
@@ -17,8 +21,5 @@ export * from './useMediaQuery';
17
21
  export * from './useMulticastObservable';
18
22
  export * from './useRefCallback';
19
23
  export * from './useViewportResize';
20
- export * from './useSignals';
21
24
  export * from './useTimeout';
22
25
  export * from './useTransitions';
23
-
24
- export { useSize, useScroller } from 'mini-virtual-list';
@@ -0,0 +1,23 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom, useAtomSet, useAtomValue } from '@effect-atom/atom-react';
6
+ import { useMemo, useState } from 'react';
7
+
8
+ export type AtomState<T> = {
9
+ atom: Atom.Writable<T>;
10
+ value: T;
11
+ set: (value: T | ((value: T) => T)) => void;
12
+ };
13
+
14
+ /**
15
+ * Wraps a writable atom together with its current value and setter.
16
+ * The atom is created once on first render; `initialValue` is only used to seed it.
17
+ */
18
+ export const useAtomState = <T>(initialValue: T): AtomState<T> => {
19
+ const [atom] = useState(() => Atom.make(initialValue));
20
+ const value = useAtomValue(atom);
21
+ const set = useAtomSet(atom);
22
+ return useMemo(() => ({ atom, value, set }), [atom, value, set]);
23
+ };
@@ -8,19 +8,19 @@ import { useDynamicRef } from './useDynamicRef';
8
8
 
9
9
  /**
10
10
  * A stateful hook with a controlled value.
11
- * @deprecated Use Radix `useControllableState` (NOTE: `useControlledState` is not compatible with `useControllableState`)
11
+ * NOTE: Consider using Radix's `useControllableState`.
12
12
  */
13
13
  export const useControlledState = <T>(
14
- valueParam: T,
14
+ valueProp: T,
15
15
  onChange?: (value: T) => void,
16
16
  ): [T, Dispatch<SetStateAction<T>>] => {
17
- const [value, setControlledValue] = useState(valueParam);
17
+ const [value, setControlledValue] = useState(valueProp);
18
18
  useEffect(() => {
19
- setControlledValue(valueParam);
20
- }, [valueParam]);
19
+ setControlledValue(valueProp);
20
+ }, [valueProp]);
21
21
 
22
22
  const onChangeRef = useRef(onChange);
23
- const valueRef = useDynamicRef(valueParam);
23
+ const valueRef = useDynamicRef(valueProp);
24
24
  const setValue = useCallback<Dispatch<SetStateAction<T>>>(
25
25
  (nextValue) => {
26
26
  const value = isFunction(nextValue) ? nextValue(valueRef.current) : nextValue;
@@ -4,23 +4,32 @@
4
4
 
5
5
  import { type DependencyList, useEffect, useRef } from 'react';
6
6
 
7
+ import { log } from '@dxos/log';
8
+
7
9
  /**
8
10
  * Util to log deps that have changed.
9
11
  */
10
- export const useDebugDeps = (deps: DependencyList = [], active = true) => {
12
+ export const useDebugDeps = (deps: DependencyList = [], label = 'useDebugDeps', active = true) => {
11
13
  const lastDeps = useRef<DependencyList>([]);
12
14
  useEffect(() => {
13
- console.group('deps changed', { previous: lastDeps.current.length, current: deps.length });
15
+ if (!active) {
16
+ return;
17
+ }
18
+
19
+ const diff: Record<number, { previous: any; current: any }> = {};
14
20
  for (let i = 0; i < Math.max(lastDeps.current.length ?? 0, deps.length ?? 0); i++) {
15
- if (lastDeps.current[i] !== deps[i] && active) {
16
- console.log('changed', {
17
- index: i,
21
+ if (lastDeps.current[i] !== deps[i] || i > lastDeps.current.length) {
22
+ diff[i] = {
18
23
  previous: lastDeps.current[i],
19
24
  current: deps[i],
20
- });
25
+ };
21
26
  }
22
27
  }
23
- console.groupEnd();
28
+
29
+ if (Object.keys(diff).length > 0) {
30
+ log.warn(`Updated: ${label} [${lastDeps.current.length}/${deps.length}]`, diff);
31
+ }
32
+
24
33
  lastDeps.current = deps;
25
- }, deps);
34
+ }, [...deps, active]);
26
35
  };
@@ -2,15 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback } from '@preact-signals/safe-react/react';
6
- import { type Dispatch, type RefObject, 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>>, RefObject<T>] => {
12
- const [value, setValue] = useState<T>(valueParam);
13
- const valueRef = useRef<T>(valueParam);
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);
14
13
  const setter = useCallback<Dispatch<SetStateAction<T>>>((value) => {
15
14
  if (typeof value === 'function') {
16
15
  setValue((current) => {
@@ -47,7 +47,9 @@ export const mergeRefs = <T>(refs: (Ref<T> | undefined)[]): Ref<T> => {
47
47
  }
48
48
 
49
49
  return () => {
50
- for (const cleanup of cleanups) cleanup();
50
+ for (const cleanup of cleanups) {
51
+ cleanup();
52
+ }
51
53
  };
52
54
  };
53
55
  };
@@ -57,5 +59,5 @@ export const mergeRefs = <T>(refs: (Ref<T> | undefined)[]): Ref<T> => {
57
59
  * The returned ref is memoized and only changes when the refs array changes.
58
60
  */
59
61
  export const useMergeRefs = <T>(refs: (Ref<T> | undefined)[]): Ref<T> => {
60
- return useMemo(() => mergeRefs(refs), refs);
62
+ return useMemo(() => mergeRefs(refs), [...refs]);
61
63
  };
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)}`;
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  // Based upon the useIsFocused hook which is part of the `rci` project:
6
- /// https://github.com/leonardodino/rci/blob/main/packages/use-is-focused
6
+ /// https://github.com/leonardodino/rci/blob/main/packages/use-w-focused
7
7
 
8
8
  import { type RefObject, useEffect, useRef, useState } from 'react';
9
9
 
@@ -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
  });
@@ -5,23 +5,48 @@
5
5
  import { useLayoutEffect, useMemo } from 'react';
6
6
 
7
7
  export const useViewportResize = (
8
- handler: (event?: Event) => void,
8
+ cb: (event?: Event) => void,
9
9
  deps: Parameters<typeof useLayoutEffect>[1] = [],
10
10
  delay: number = 800,
11
11
  ) => {
12
- const debouncedHandler = useMemo(() => {
13
- let timeout: ReturnType<typeof setTimeout>;
14
- return (event?: Event) => {
15
- clearTimeout(timeout);
16
- timeout = setTimeout(() => {
17
- handler(event);
18
- }, delay);
12
+ // The cleanup must cancel the pending debounce timeout. Otherwise, if the
13
+ // component unmounts during the `delay` window (common in jsdom/happy-dom
14
+ // test teardown), the callback fires against a torn-down DOM and surfaces
15
+ // as `ReferenceError: getComputedStyle is not defined`.
16
+ const { handler: debouncedHandler, cancel } = useMemo(() => {
17
+ let timeout: ReturnType<typeof setTimeout> | undefined;
18
+ return {
19
+ handler: (event?: Event) => {
20
+ if (timeout !== undefined) {
21
+ clearTimeout(timeout);
22
+ }
23
+ timeout = setTimeout(() => {
24
+ timeout = undefined;
25
+ // The debounced callback can outlive the DOM realm: in jsdom/happy-dom test runs the
26
+ // timer survives environment teardown (e.g. story roots that are never unmounted), and
27
+ // callbacks here read DOM globals such as `getComputedStyle`. Skip dispatch once the
28
+ // realm is gone.
29
+ if (typeof document === 'undefined' || typeof getComputedStyle === 'undefined') {
30
+ return;
31
+ }
32
+ cb(event);
33
+ }, delay);
34
+ },
35
+ cancel: () => {
36
+ if (timeout !== undefined) {
37
+ clearTimeout(timeout);
38
+ timeout = undefined;
39
+ }
40
+ },
19
41
  };
20
- }, [handler, delay]);
42
+ }, [cb, delay]);
21
43
 
22
44
  return useLayoutEffect(() => {
23
45
  window.visualViewport?.addEventListener('resize', debouncedHandler);
24
46
  debouncedHandler();
25
- return () => window.visualViewport?.removeEventListener('resize', debouncedHandler);
26
- }, [debouncedHandler, ...deps]);
47
+ return () => {
48
+ window.visualViewport?.removeEventListener('resize', debouncedHandler);
49
+ cancel();
50
+ };
51
+ }, [debouncedHandler, cancel, ...deps]);
27
52
  };
@@ -1,10 +0,0 @@
1
- import { type DependencyList } from 'react';
2
- /**
3
- * Like `useEffect` but also tracks signals inside of the callback.
4
- */
5
- export declare const useSignalsEffect: (cb: () => void | (() => void), deps?: DependencyList) => void;
6
- /**
7
- * Like `useMemo` but also tracks signals inside of the callback.
8
- */
9
- export declare const useSignalsMemo: <T>(cb: () => T, deps?: DependencyList) => T;
10
- //# sourceMappingURL=useSignals.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useSignals.d.ts","sourceRoot":"","sources":["../../../src/useSignals.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,OAAO,CAAC;AAEhE;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAAI,IAAI,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,OAAO,cAAc,SAQpF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,OAAO,cAAc,MAEnE,CAAC"}
package/src/useSignals.ts DELETED
@@ -1,27 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
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';
8
-
9
- /**
10
- * Like `useEffect` but also tracks signals inside of the callback.
11
- */
12
- export const useSignalsEffect = (cb: () => void | (() => void), deps?: DependencyList) => {
13
- const callback = useRef(cb);
14
- callback.current = cb;
15
- useEffect(() => {
16
- return effect(() => {
17
- return callback.current();
18
- });
19
- }, deps ?? []);
20
- };
21
-
22
- /**
23
- * Like `useMemo` but also tracks signals inside of the callback.
24
- */
25
- export const useSignalsMemo = <T>(cb: () => T, deps?: DependencyList) => {
26
- return useMemo(() => computed(cb), deps ?? []).value;
27
- };