diff --git a/packages/react-bindings/package.json b/packages/react-bindings/package.json index 616666759f..9d4a4a81ef 100644 --- a/packages/react-bindings/package.json +++ b/packages/react-bindings/package.json @@ -22,7 +22,8 @@ ], "dependencies": { "i18next": "^25.2.1", - "rxjs": "~7.8.1" + "rxjs": "~7.8.1", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@stream-io/video-client": "workspace:^", @@ -32,6 +33,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@stream-io/video-client": "workspace:^", "@types/react": "^19.2.0", + "@types/use-sync-external-store": "^1", "react": "19.0.0", "rimraf": "^6.0.1", "rollup": "^4.40.2", diff --git a/packages/react-bindings/src/hooks/callStateHooks.ts b/packages/react-bindings/src/hooks/callStateHooks.ts index 193f111191..df7fc5a9a1 100644 --- a/packages/react-bindings/src/hooks/callStateHooks.ts +++ b/packages/react-bindings/src/hooks/callStateHooks.ts @@ -525,7 +525,7 @@ function useLazyDeviceList(manager: DeviceManagerLike) { setDevices$(manager.listDevices()); } - return devices; + return devices ?? EMPTY_DEVICES_ARRAY; }; return { getDevices }; diff --git a/packages/react-bindings/src/hooks/useObservableValue.ts b/packages/react-bindings/src/hooks/useObservableValue.ts index dc31c1c95e..bd735236f2 100644 --- a/packages/react-bindings/src/hooks/useObservableValue.ts +++ b/packages/react-bindings/src/hooks/useObservableValue.ts @@ -1,32 +1,43 @@ import type { Observable } from 'rxjs'; -import { useEffect, useState } from 'react'; +import { useCallback } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; import { RxUtils } from '@stream-io/video-client'; /** * Utility hook which provides the current value of the given observable. * * @param observable$ the observable to read data from. - * @param defaultValue a default value. Used when the observable data can't be read or emits an error. + * @param defaultValue a default value. Used when the observable data can't be read or emits an error - must be stable. */ export const useObservableValue = ( observable$: Observable, defaultValue?: T, ) => { - const [value, setValue] = useState(() => { + const getSnapshot = useCallback(() => { try { return RxUtils.getCurrentValue(observable$); - } catch (err) { - if (typeof defaultValue === 'undefined') throw err; + } catch (error) { + if (typeof defaultValue === 'undefined') throw error; return defaultValue; } - }); - - useEffect(() => { - return RxUtils.createSubscription(observable$, setValue, (err) => { - console.log('An error occurred while reading an observable', err); - if (defaultValue) setValue(defaultValue); - }); }, [defaultValue, observable$]); - return value; + const subscribe = useCallback( + (onStoreChange: (v: T) => void) => { + const unsubscribe = RxUtils.createSubscription( + observable$, + onStoreChange, + (error) => { + console.log('An error occurred while reading an observable', error); + + if (defaultValue) onStoreChange(defaultValue); + }, + ); + + return unsubscribe; + }, + [defaultValue, observable$], + ); + + return useSyncExternalStore(subscribe, getSnapshot); }; diff --git a/yarn.lock b/yarn.lock index 79d7daf24e..d27d5d3b6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8763,12 +8763,14 @@ __metadata: "@rollup/plugin-typescript": "npm:^12.1.2" "@stream-io/video-client": "workspace:^" "@types/react": "npm:^19.2.0" + "@types/use-sync-external-store": "npm:^1" i18next: "npm:^25.2.1" react: "npm:19.0.0" rimraf: "npm:^6.0.1" rollup: "npm:^4.40.2" rxjs: "npm:~7.8.1" typescript: "npm:^5.8.3" + use-sync-external-store: "npm:^1.6.0" peerDependencies: "@stream-io/video-client": "workspace:^" react: ^17 || ^18 || ^19 @@ -9658,6 +9660,13 @@ __metadata: languageName: node linkType: hard +"@types/use-sync-external-store@npm:^1": + version: 1.5.0 + resolution: "@types/use-sync-external-store@npm:1.5.0" + checksum: 10/39e5be8dc2cca080b490f2f79fed4381ae7eebee3f981208e359856733eafb2479d229db07a552f6c99fe0b5c09b3e46a3e6a870e00a88b50f3e690e73d2649b + languageName: node + linkType: hard + "@types/ws@npm:^8.5.14": version: 8.18.1 resolution: "@types/ws@npm:8.18.1" @@ -27008,6 +27017,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.6.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/b40ad2847ba220695bff2d4ba4f4d60391c0fb4fb012faa7a4c18eb38b69181936f5edc55a522c4d20a788d1a879b73c3810952c9d0fd128d01cb3f22042c09e + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2"