@dxos/react-hooks 0.6.11-staging.e6894a4 → 0.6.12-main.15a606f
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 +92 -32
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +91 -34
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +324 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/types/src/index.d.ts +4 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/useAsyncEffect.d.ts +30 -0
- package/dist/types/src/useAsyncEffect.d.ts.map +1 -0
- package/dist/types/src/useAsyncEffect.test.d.ts +2 -0
- package/dist/types/src/useAsyncEffect.test.d.ts.map +1 -0
- package/dist/types/src/useAsyncState.d.ts +5 -0
- package/dist/types/src/useAsyncState.d.ts.map +1 -0
- package/dist/types/src/useDebugReactDeps.d.ts +6 -0
- package/dist/types/src/useDebugReactDeps.d.ts.map +1 -0
- package/dist/types/src/useDefaultValue.d.ts.map +1 -1
- package/dist/types/src/useDynamicRef.d.ts.map +1 -1
- package/dist/types/src/useMulticastObservable.d.ts +7 -0
- package/dist/types/src/useMulticastObservable.d.ts.map +1 -0
- package/dist/types/src/useMulticastObservable.test.d.ts +2 -0
- package/dist/types/src/useMulticastObservable.test.d.ts.map +1 -0
- package/dist/types/src/useTransitions.d.ts +1 -1
- package/dist/types/src/useTransitions.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/index.ts +4 -1
- package/src/useAsyncEffect.test.tsx +51 -0
- package/src/useAsyncEffect.ts +61 -0
- package/src/{useAsyncCallback.ts → useAsyncState.ts} +5 -2
- package/src/useDebugReactDeps.ts +27 -0
- package/src/useDefaultValue.ts +3 -1
- package/src/useDynamicRef.ts +1 -0
- package/src/useMulticastObservable.test.tsx +23 -0
- package/src/useMulticastObservable.ts +26 -0
- package/src/useTransitions.ts +4 -7
- package/dist/types/src/useAsyncCallback.d.ts +0 -2
- package/dist/types/src/useAsyncCallback.d.ts.map +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useState } from 'react';
|
|
6
|
+
import { createRoot } from 'react-dom/client';
|
|
7
|
+
import { act } from 'react-dom/test-utils';
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { useAsyncEffect } from './useAsyncEffect';
|
|
11
|
+
|
|
12
|
+
const doAsync = async <T,>(value: T) =>
|
|
13
|
+
await new Promise<T>((resolve) => {
|
|
14
|
+
resolve(value);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const Test = () => {
|
|
18
|
+
const [value, setValue] = useState<string>();
|
|
19
|
+
useAsyncEffect(async (isMounted) => {
|
|
20
|
+
const value = await doAsync('DXOS');
|
|
21
|
+
if (isMounted()) {
|
|
22
|
+
void act(() => {
|
|
23
|
+
setValue(value);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return <h1>{value}</h1>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let rootContainer: HTMLElement;
|
|
32
|
+
|
|
33
|
+
describe('useAsyncEffect', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
rootContainer = document.createElement('div');
|
|
36
|
+
document.body.appendChild(rootContainer);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
document.body.removeChild(rootContainer!);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('gets async value.', async () => {
|
|
44
|
+
await act(() => {
|
|
45
|
+
createRoot(rootContainer).render(<Test />);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const h1 = rootContainer.querySelector('h1');
|
|
49
|
+
expect(h1?.textContent).toEqual('DXOS');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
import { log } from '@dxos/log';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Process async event with optional non-async destructor.
|
|
11
|
+
* Inspired by: https://github.com/rauldeheer/use-async-effect/blob/master/index.js
|
|
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
|
+
export const useAsyncEffect = <T>(
|
|
38
|
+
callback: (isMounted: () => boolean) => Promise<T> | undefined,
|
|
39
|
+
destructor?: ((value?: T) => void) | any[],
|
|
40
|
+
deps?: any[],
|
|
41
|
+
) => {
|
|
42
|
+
const [effectDestructor, effectDeps] =
|
|
43
|
+
typeof destructor === 'function' ? [destructor, deps] : [undefined, destructor];
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let mounted = true;
|
|
47
|
+
let value: T | undefined;
|
|
48
|
+
const asyncResult = callback(() => mounted);
|
|
49
|
+
|
|
50
|
+
void Promise.resolve(asyncResult)
|
|
51
|
+
.then((result) => {
|
|
52
|
+
value = result;
|
|
53
|
+
})
|
|
54
|
+
.catch(log.catch);
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
mounted = false;
|
|
58
|
+
effectDestructor?.(value);
|
|
59
|
+
};
|
|
60
|
+
}, effectDeps);
|
|
61
|
+
};
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* NOTE: Use with care and when necessary to be able to cancel an async operation when unmounting.
|
|
9
|
+
*/
|
|
10
|
+
export const useAsyncState = <T>(cb: () => Promise<T | undefined>, deps: any[] = []): T | undefined => {
|
|
8
11
|
const [value, setValue] = useState<T | undefined>();
|
|
9
12
|
useEffect(() => {
|
|
10
13
|
const t = setTimeout(async () => {
|
|
@@ -13,7 +16,7 @@ export const useAsyncCallback = <T>(cb: () => Promise<T>): T | undefined => {
|
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
return () => clearTimeout(t);
|
|
16
|
-
},
|
|
19
|
+
}, deps);
|
|
17
20
|
|
|
18
21
|
return value;
|
|
19
22
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
/* eslint-disable no-console */
|
|
6
|
+
|
|
7
|
+
import { type DependencyList, useEffect, useRef } from 'react';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Util to log deps that have changed.
|
|
11
|
+
*/
|
|
12
|
+
// TODO(burdon): Move to react-hooks.
|
|
13
|
+
export const useDebugReactDeps = (deps: DependencyList = []) => {
|
|
14
|
+
const lastDeps = useRef<DependencyList>([]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
console.group('deps changed', { old: lastDeps.current.length, new: deps.length });
|
|
17
|
+
for (let i = 0; i < Math.max(lastDeps.current.length ?? 0, deps.length ?? 0); i++) {
|
|
18
|
+
console.log(i, lastDeps.current[i] === deps[i] ? 'SAME' : 'CHANGED', {
|
|
19
|
+
previous: lastDeps.current[i],
|
|
20
|
+
current: deps[i],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.groupEnd();
|
|
25
|
+
lastDeps.current = deps;
|
|
26
|
+
}, deps);
|
|
27
|
+
};
|
package/src/useDefaultValue.ts
CHANGED
|
@@ -22,7 +22,9 @@ export const useDefaultValue = <T>(reactiveValue: T | undefined | null, defaultV
|
|
|
22
22
|
const stableDefaultValue = useMemo(() => defaultValue, []);
|
|
23
23
|
const [value, setValue] = useState(reactiveValue ?? stableDefaultValue);
|
|
24
24
|
|
|
25
|
-
useEffect(() =>
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setValue(reactiveValue ?? stableDefaultValue);
|
|
27
|
+
}, [reactiveValue, stableDefaultValue]);
|
|
26
28
|
|
|
27
29
|
return value;
|
|
28
30
|
};
|
package/src/useDynamicRef.ts
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { act, renderHook } from '@testing-library/react';
|
|
6
|
+
import { describe, expect, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { Event, MulticastObservable } from '@dxos/async';
|
|
9
|
+
|
|
10
|
+
import { useMulticastObservable } from './useMulticastObservable';
|
|
11
|
+
|
|
12
|
+
describe('useMulticastObservable', () => {
|
|
13
|
+
test('observes value', async () => {
|
|
14
|
+
const event = new Event<number>();
|
|
15
|
+
const observable = MulticastObservable.from(event, 0);
|
|
16
|
+
const { result } = renderHook(() => useMulticastObservable(observable));
|
|
17
|
+
expect(result.current).toEqual(0);
|
|
18
|
+
|
|
19
|
+
await act(() => event.emit(1));
|
|
20
|
+
|
|
21
|
+
await expect.poll(() => result.current).toEqual(1);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useMemo, useSyncExternalStore } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type MulticastObservable } from '@dxos/async';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to a MulticastObservable and return the latest value.
|
|
11
|
+
* @param observable the observable to subscribe to. Will resubscribe if the observable changes.
|
|
12
|
+
*/
|
|
13
|
+
// TODO(burdon): Move to react-hooks.
|
|
14
|
+
export const useMulticastObservable = <T>(observable: MulticastObservable<T>): T => {
|
|
15
|
+
// Make sure useSyncExternalStore is stable in respect to the observable.
|
|
16
|
+
const subscribeFn = useMemo(
|
|
17
|
+
() => (listener: () => void) => {
|
|
18
|
+
const subscription = observable.subscribe(listener);
|
|
19
|
+
return () => subscription.unsubscribe();
|
|
20
|
+
},
|
|
21
|
+
[observable],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// useSyncExternalStore will resubscribe to the observable and update the value if the subscribeFn changes.
|
|
25
|
+
return useSyncExternalStore(subscribeFn, () => observable.get());
|
|
26
|
+
};
|
package/src/useTransitions.ts
CHANGED
|
@@ -28,21 +28,18 @@ export const useDidTransition = <T>(
|
|
|
28
28
|
|
|
29
29
|
useEffect(() => {
|
|
30
30
|
const toValueValid = isFunction<T>(toValue) ? toValue(currentValue) : toValue === currentValue;
|
|
31
|
-
|
|
32
31
|
const fromValueValid = isFunction<T>(fromValue)
|
|
33
32
|
? fromValue(previousValue.current)
|
|
34
33
|
: fromValue === previousValue.current;
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (transitioned) {
|
|
35
|
+
if (fromValueValid && toValueValid && !hasTransitioned) {
|
|
39
36
|
setHasTransitioned(true);
|
|
40
|
-
} else {
|
|
37
|
+
} else if ((!fromValueValid || !toValueValid) && hasTransitioned) {
|
|
41
38
|
setHasTransitioned(false);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
previousValue.current = currentValue;
|
|
45
|
-
}, [currentValue, fromValue, toValue,
|
|
42
|
+
}, [currentValue, fromValue, toValue, hasTransitioned]);
|
|
46
43
|
|
|
47
44
|
return hasTransitioned;
|
|
48
45
|
};
|
|
@@ -55,7 +52,7 @@ export const useDidTransition = <T>(
|
|
|
55
52
|
export const useOnTransition = <T>(
|
|
56
53
|
currentValue: T,
|
|
57
54
|
fromValue: T | ((value: T) => boolean),
|
|
58
|
-
toValue: ((value: T) => boolean)
|
|
55
|
+
toValue: T | ((value: T) => boolean),
|
|
59
56
|
callback: () => void,
|
|
60
57
|
) => {
|
|
61
58
|
const dirty = useRef(false);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useAsyncCallback.d.ts","sourceRoot":"","sources":["../../../src/useAsyncCallback.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,GAAI,CAAC,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,CAAC,GAAG,SAY9D,CAAC"}
|