Skip to content

Commit 1af9c62

Browse files
authored
fix: useStateObservable (#253)
1 parent 6fe91f8 commit 1af9c62

File tree

6 files changed

+101
-96
lines changed

6 files changed

+101
-96
lines changed

packages/core/src/Subscribe.test.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { render } from "@testing-library/react"
1+
import { state } from "@josepot/rxjs-state"
2+
import { render, screen } from "@testing-library/react"
23
import React, { StrictMode, useState } from "react"
3-
import { defer, Observable, of } from "rxjs"
4+
import { defer, EMPTY, Observable, of, startWith } from "rxjs"
5+
import { useStateObservable } from "./useStateObservable"
46
import { bind, Subscribe as OriginalSubscribe } from "./"
57

68
const Subscribe = (props: any) => {
@@ -13,6 +15,24 @@ const Subscribe = (props: any) => {
1315

1416
describe("Subscribe", () => {
1517
describe("Subscribe with source$", () => {
18+
it("renders the sync emitted value on a StateObservable without default value", () => {
19+
const test$ = state(EMPTY.pipe(startWith("there!")))
20+
const useTest = () => useStateObservable(test$)
21+
22+
const Test: React.FC = () => <>Hello {useTest()}</>
23+
24+
const TestSubscribe: React.FC = () => (
25+
<Subscribe>
26+
<Test />
27+
</Subscribe>
28+
)
29+
30+
const { unmount } = render(<TestSubscribe />)
31+
32+
expect(screen.queryByText("Hello there!")).not.toBeNull()
33+
34+
unmount()
35+
})
1636
it("subscribes to the provided observable and remains subscribed until it's unmounted", () => {
1737
let nSubscriptions = 0
1838
const [useNumber, number$] = bind(

packages/core/src/Subscribe.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React, {
99
} from "react"
1010
import { Observable, Subscription } from "rxjs"
1111

12-
const SubscriptionContext = createContext<Subscription>(null as any)
12+
const SubscriptionContext = createContext<Subscription | null>(null)
1313
const { Provider } = SubscriptionContext
1414
export const useSubscription = () => useContext(SubscriptionContext)
1515

packages/core/src/bind/connectFactoryObservable.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { noop, Observable } from "rxjs"
2-
import { useObservable } from "../internal/useObservable"
1+
import { Observable } from "rxjs"
32
import { SUSPENSE } from "../SUSPENSE"
43
import { EMPTY_VALUE } from "../internal/empty-value"
5-
import { useSubscription } from "../Subscribe"
64
import { state, StateObservable } from "@josepot/rxjs-state"
5+
import { useStateObservable } from "../useStateObservable"
76

87
/**
98
* Accepts: A factory function that returns an Observable.
@@ -39,9 +38,5 @@ export default function connectFactoryObservable<A extends [], O>(
3938
: [getObservable, defaultValue]
4039

4140
const obs = state(...(args as [(...args: A) => Observable<O>]))
42-
const useSub = defaultValue === EMPTY_VALUE ? useSubscription : noop
43-
return [
44-
(...input: A) => useObservable(obs(...input) as any, useSub() as any),
45-
obs,
46-
]
41+
return [(...input: A) => useStateObservable(obs(...input)), obs]
4742
}

packages/core/src/bind/connectObservable.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { EMPTY_VALUE } from "../internal/empty-value"
2-
import { noop, Observable } from "rxjs"
3-
import { useSubscription } from "../Subscribe"
4-
import { useObservable } from "../internal/useObservable"
2+
import { Observable } from "rxjs"
3+
import { useStateObservable } from "../useStateObservable"
54
import { state } from "@josepot/rxjs-state"
65

76
/**
@@ -23,13 +22,11 @@ export default function connectObservable<T>(
2322
observable: Observable<T>,
2423
defaultValue: T,
2524
) {
26-
const useSub = defaultValue === EMPTY_VALUE ? useSubscription : noop
2725
const sharedObservable$ =
2826
defaultValue === EMPTY_VALUE
2927
? state(observable)
3028
: state(observable, defaultValue)
3129

32-
const useStaticObservable = () =>
33-
useObservable(sharedObservable$ as any, useSub() as any)
30+
const useStaticObservable = () => useStateObservable(sharedObservable$ as any)
3431
return [useStaticObservable, sharedObservable$] as const
3532
}

packages/core/src/internal/useObservable.ts

Lines changed: 0 additions & 73 deletions
This file was deleted.
Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,73 @@
1-
import { useObservable } from "./internal/useObservable"
2-
import { SUSPENSE } from "./SUSPENSE"
3-
import type { StateObservable } from "@josepot/rxjs-state"
1+
import { useRef, useState } from "react"
2+
import { SUSPENSE, filterOutSuspense } from "./SUSPENSE"
3+
import { DefaultedStateObservable, StateObservable } from "@josepot/rxjs-state"
4+
import { EMPTY_VALUE } from "./internal/empty-value"
5+
import useSyncExternalStore from "./internal/useSyncExternalStore"
6+
import { useSubscription } from "./Subscribe"
47

5-
export const useStateObservable = <T>(
6-
stateObservable: StateObservable<T>,
7-
): Exclude<T, typeof SUSPENSE> => useObservable(stateObservable)
8+
type VoidCb = () => void
9+
10+
interface Ref<T> {
11+
source$: StateObservable<T>
12+
args: [(cb: VoidCb) => VoidCb, () => Exclude<T, typeof SUSPENSE>]
13+
}
14+
15+
export const useStateObservable = <O>(
16+
source$: StateObservable<O>,
17+
): Exclude<O, typeof SUSPENSE> => {
18+
const subscription = useSubscription()
19+
const [, setError] = useState()
20+
const callbackRef = useRef<Ref<O>>()
21+
22+
if (!callbackRef.current) {
23+
const getValue = (src: StateObservable<O>) => {
24+
const result = src.getValue(filterOutSuspense)
25+
if (result instanceof Promise) throw result
26+
return result as any
27+
}
28+
29+
const gv: <T>() => Exclude<T, typeof SUSPENSE> = () => {
30+
const src = callbackRef.current!.source$ as DefaultedStateObservable<O>
31+
32+
if (src.getRefCount() > 0 || src.getDefaultValue) return getValue(src)
33+
34+
if (!subscription) throw new Error("Missing Subscribe!")
35+
36+
let error = EMPTY_VALUE
37+
subscription.add(
38+
src.subscribe({
39+
error: (e) => {
40+
error = e
41+
},
42+
}),
43+
)
44+
if (error !== EMPTY_VALUE) throw error
45+
return getValue(src)
46+
}
47+
48+
callbackRef.current = {
49+
source$: null as any,
50+
args: [, gv] as any,
51+
}
52+
}
53+
54+
const ref = callbackRef.current
55+
if (ref.source$ !== source$) {
56+
ref.source$ = source$
57+
ref.args[0] = (next: () => void) => {
58+
const subscription = source$.subscribe({
59+
next,
60+
error: (e) => {
61+
setError(() => {
62+
throw e
63+
})
64+
},
65+
})
66+
return () => {
67+
subscription.unsubscribe()
68+
}
69+
}
70+
}
71+
72+
return useSyncExternalStore(...ref!.args)
73+
}

0 commit comments

Comments
 (0)