diff --git a/packages/core/src/Subscribe.test.tsx b/packages/core/src/Subscribe.test.tsx index 1fef0d0..1353926 100644 --- a/packages/core/src/Subscribe.test.tsx +++ b/packages/core/src/Subscribe.test.tsx @@ -1,6 +1,8 @@ -import { render } from "@testing-library/react" +import { state } from "@josepot/rxjs-state" +import { render, screen } from "@testing-library/react" import React, { StrictMode, useState } from "react" -import { defer, Observable, of } from "rxjs" +import { defer, EMPTY, Observable, of, startWith } from "rxjs" +import { useStateObservable } from "./useStateObservable" import { bind, Subscribe as OriginalSubscribe } from "./" const Subscribe = (props: any) => { @@ -13,6 +15,24 @@ const Subscribe = (props: any) => { describe("Subscribe", () => { describe("Subscribe with source$", () => { + it("renders the sync emitted value on a StateObservable without default value", () => { + const test$ = state(EMPTY.pipe(startWith("there!"))) + const useTest = () => useStateObservable(test$) + + const Test: React.FC = () => <>Hello {useTest()} + + const TestSubscribe: React.FC = () => ( + + + + ) + + const { unmount } = render() + + expect(screen.queryByText("Hello there!")).not.toBeNull() + + unmount() + }) it("subscribes to the provided observable and remains subscribed until it's unmounted", () => { let nSubscriptions = 0 const [useNumber, number$] = bind( diff --git a/packages/core/src/Subscribe.tsx b/packages/core/src/Subscribe.tsx index 95e9ddb..2487505 100644 --- a/packages/core/src/Subscribe.tsx +++ b/packages/core/src/Subscribe.tsx @@ -9,7 +9,7 @@ import React, { } from "react" import { Observable, Subscription } from "rxjs" -const SubscriptionContext = createContext(null as any) +const SubscriptionContext = createContext(null) const { Provider } = SubscriptionContext export const useSubscription = () => useContext(SubscriptionContext) diff --git a/packages/core/src/bind/connectFactoryObservable.ts b/packages/core/src/bind/connectFactoryObservable.ts index 981b11f..2ba0953 100644 --- a/packages/core/src/bind/connectFactoryObservable.ts +++ b/packages/core/src/bind/connectFactoryObservable.ts @@ -1,9 +1,8 @@ -import { noop, Observable } from "rxjs" -import { useObservable } from "../internal/useObservable" +import { Observable } from "rxjs" import { SUSPENSE } from "../SUSPENSE" import { EMPTY_VALUE } from "../internal/empty-value" -import { useSubscription } from "../Subscribe" import { state, StateObservable } from "@josepot/rxjs-state" +import { useStateObservable } from "../useStateObservable" /** * Accepts: A factory function that returns an Observable. @@ -39,9 +38,5 @@ export default function connectFactoryObservable( : [getObservable, defaultValue] const obs = state(...(args as [(...args: A) => Observable])) - const useSub = defaultValue === EMPTY_VALUE ? useSubscription : noop - return [ - (...input: A) => useObservable(obs(...input) as any, useSub() as any), - obs, - ] + return [(...input: A) => useStateObservable(obs(...input)), obs] } diff --git a/packages/core/src/bind/connectObservable.ts b/packages/core/src/bind/connectObservable.ts index 03eacc9..9247b68 100644 --- a/packages/core/src/bind/connectObservable.ts +++ b/packages/core/src/bind/connectObservable.ts @@ -1,7 +1,6 @@ import { EMPTY_VALUE } from "../internal/empty-value" -import { noop, Observable } from "rxjs" -import { useSubscription } from "../Subscribe" -import { useObservable } from "../internal/useObservable" +import { Observable } from "rxjs" +import { useStateObservable } from "../useStateObservable" import { state } from "@josepot/rxjs-state" /** @@ -23,13 +22,11 @@ export default function connectObservable( observable: Observable, defaultValue: T, ) { - const useSub = defaultValue === EMPTY_VALUE ? useSubscription : noop const sharedObservable$ = defaultValue === EMPTY_VALUE ? state(observable) : state(observable, defaultValue) - const useStaticObservable = () => - useObservable(sharedObservable$ as any, useSub() as any) + const useStaticObservable = () => useStateObservable(sharedObservable$ as any) return [useStaticObservable, sharedObservable$] as const } diff --git a/packages/core/src/internal/useObservable.ts b/packages/core/src/internal/useObservable.ts deleted file mode 100644 index bdb1e66..0000000 --- a/packages/core/src/internal/useObservable.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Subscription } from "rxjs" -import { useRef, useState } from "react" -import { SUSPENSE, filterOutSuspense } from "../SUSPENSE" -import { DefaultedStateObservable, StateObservable } from "@josepot/rxjs-state" -import { EMPTY_VALUE } from "./empty-value" -import useSyncExternalStore from "./useSyncExternalStore" - -type VoidCb = () => void - -interface Ref { - source$: StateObservable - args: [(cb: VoidCb) => VoidCb, () => Exclude] -} - -export const useObservable = ( - source$: StateObservable, - subscription?: Subscription, -): Exclude => { - const [, setError] = useState() - const callbackRef = useRef>() - - if (!callbackRef.current) { - const getValue = (src: StateObservable) => { - const result = src.getValue(filterOutSuspense) - if (result instanceof Promise) throw result - return result as any - } - - const gv: () => Exclude = () => { - const src = callbackRef.current!.source$ as DefaultedStateObservable - - if (src.getRefCount() > 0 || src.getDefaultValue) return getValue(src) - - if (!subscription) throw new Error("Missing Subscribe!") - - let error = EMPTY_VALUE - subscription.add( - src.subscribe({ - error: (e) => { - error = e - }, - }), - ) - if (error !== EMPTY_VALUE) throw error - return getValue(src) - } - - callbackRef.current = { - source$: null as any, - args: [, gv] as any, - } - } - - const ref = callbackRef.current - if (ref.source$ !== source$) { - ref.source$ = source$ - ref.args[0] = (next: () => void) => { - const subscription = source$.subscribe({ - next, - error: (e) => { - setError(() => { - throw e - }) - }, - }) - return () => { - subscription.unsubscribe() - } - } - } - - return useSyncExternalStore(...ref!.args) -} diff --git a/packages/core/src/useStateObservable.ts b/packages/core/src/useStateObservable.ts index 58ef5a4..d2c8b51 100644 --- a/packages/core/src/useStateObservable.ts +++ b/packages/core/src/useStateObservable.ts @@ -1,7 +1,73 @@ -import { useObservable } from "./internal/useObservable" -import { SUSPENSE } from "./SUSPENSE" -import type { StateObservable } from "@josepot/rxjs-state" +import { useRef, useState } from "react" +import { SUSPENSE, filterOutSuspense } from "./SUSPENSE" +import { DefaultedStateObservable, StateObservable } from "@josepot/rxjs-state" +import { EMPTY_VALUE } from "./internal/empty-value" +import useSyncExternalStore from "./internal/useSyncExternalStore" +import { useSubscription } from "./Subscribe" -export const useStateObservable = ( - stateObservable: StateObservable, -): Exclude => useObservable(stateObservable) +type VoidCb = () => void + +interface Ref { + source$: StateObservable + args: [(cb: VoidCb) => VoidCb, () => Exclude] +} + +export const useStateObservable = ( + source$: StateObservable, +): Exclude => { + const subscription = useSubscription() + const [, setError] = useState() + const callbackRef = useRef>() + + if (!callbackRef.current) { + const getValue = (src: StateObservable) => { + const result = src.getValue(filterOutSuspense) + if (result instanceof Promise) throw result + return result as any + } + + const gv: () => Exclude = () => { + const src = callbackRef.current!.source$ as DefaultedStateObservable + + if (src.getRefCount() > 0 || src.getDefaultValue) return getValue(src) + + if (!subscription) throw new Error("Missing Subscribe!") + + let error = EMPTY_VALUE + subscription.add( + src.subscribe({ + error: (e) => { + error = e + }, + }), + ) + if (error !== EMPTY_VALUE) throw error + return getValue(src) + } + + callbackRef.current = { + source$: null as any, + args: [, gv] as any, + } + } + + const ref = callbackRef.current + if (ref.source$ !== source$) { + ref.source$ = source$ + ref.args[0] = (next: () => void) => { + const subscription = source$.subscribe({ + next, + error: (e) => { + setError(() => { + throw e + }) + }, + }) + return () => { + subscription.unsubscribe() + } + } + } + + return useSyncExternalStore(...ref!.args) +}