Skip to content

Commit 90e6a00

Browse files
committed
@react-rxjs/core@v0.6.0
1 parent 9f5f6d6 commit 90e6a00

14 files changed

+296
-228
lines changed

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.5.0",
2+
"version": "0.6.0",
33
"repository": {
44
"type": "git",
55
"url": "git+https://github.com/re-rxjs/react-rxjs.git"

packages/core/src/Subscribe.test.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import React from "react"
22
import { render } from "@testing-library/react"
3-
import { defer, Subject } from "rxjs"
4-
import { share, finalize } from "rxjs/operators"
5-
import { Subscribe } from "./"
3+
import { Observable } from "rxjs"
4+
import { Subscribe, bind } from "./"
65

76
describe("Subscribe", () => {
87
it("subscribes to the provided observable and remains subscribed until it's unmounted", () => {
98
let nSubscriptions = 0
10-
const source$ = defer(() => {
11-
nSubscriptions++
12-
return new Subject()
13-
}).pipe(
14-
finalize(() => {
15-
nSubscriptions--
9+
const [useNumber, number$] = bind(
10+
new Observable<number>(() => {
11+
nSubscriptions++
12+
return () => {
13+
nSubscriptions--
14+
}
1615
}),
17-
share(),
1816
)
1917

20-
const TestSubscribe: React.FC = () => <Subscribe source$={source$} />
18+
const Number: React.FC = () => <>{useNumber()}</>
19+
const TestSubscribe: React.FC = () => (
20+
<Subscribe source$={number$}>
21+
<Number />
22+
</Subscribe>
23+
)
2124

2225
expect(nSubscriptions).toBe(0)
2326

packages/core/src/Subscribe.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import React, { useState, useEffect } from "react"
2-
import { Observable } from "rxjs"
1+
import React, { useState, Suspense, useLayoutEffect, ReactNode } from "react"
2+
import { Observable, noop } from "rxjs"
3+
4+
const p = Promise.resolve()
5+
const Throw = () => {
6+
throw p
7+
}
38

49
/**
510
* A React Component that creates a subscription to the provided observable once
@@ -12,15 +17,27 @@ import { Observable } from "rxjs"
1217
*/
1318
export const Subscribe: React.FC<{
1419
source$: Observable<any>
15-
fallback?: null | JSX.Element
20+
fallback?: NonNullable<ReactNode> | null
1621
}> = ({ source$, children, fallback }) => {
17-
const [mounted, setMounted] = useState(0)
18-
useEffect(() => {
19-
const subscription = source$.subscribe()
22+
const [mounted, setMounted] = useState(() => {
23+
try {
24+
;(source$ as any).gV()
25+
return 1
26+
} catch (e) {
27+
return e.then ? 1 : 0
28+
}
29+
})
30+
useLayoutEffect(() => {
31+
const subscription = source$.subscribe(noop, (e) =>
32+
setMounted(() => {
33+
throw e
34+
}),
35+
)
2036
setMounted(1)
2137
return () => {
2238
subscription.unsubscribe()
2339
}
2440
}, [source$])
25-
return <>{mounted ? children : fallback}</>
41+
const fBack = fallback || null
42+
return <Suspense fallback={fBack}>{mounted ? children : <Throw />}</Suspense>
2643
}

packages/core/src/bind/connectFactoryObservable.test.tsx

Lines changed: 97 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@ import {
33
of,
44
defer,
55
concat,
6-
BehaviorSubject,
76
throwError,
87
Observable,
98
Subject,
9+
merge,
1010
} from "rxjs"
1111
import { renderHook, act as actHook } from "@testing-library/react-hooks"
12-
import { switchMap, delay, take, catchError, map } from "rxjs/operators"
13-
import { FC, Suspense, useState } from "react"
12+
import { delay, take, catchError, map, switchMapTo } from "rxjs/operators"
13+
import { FC, useState } from "react"
1414
import React from "react"
1515
import {
1616
act as componentAct,
1717
fireEvent,
1818
screen,
1919
render,
2020
} from "@testing-library/react"
21-
import { bind } from "../"
21+
import { bind, Subscribe } from "../"
2222
import { TestErrorBoundary } from "../test-helpers/TestErrorBoundary"
2323

2424
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
@@ -42,8 +42,8 @@ describe("connectFactoryObservable", () => {
4242
})
4343
describe("hook", () => {
4444
it("returns the latest emitted value", async () => {
45-
const valueStream = new BehaviorSubject(1)
46-
const [useNumber] = bind(() => valueStream)
45+
const valueStream = new Subject<number>()
46+
const [useNumber] = bind(() => valueStream, 1)
4747
const { result } = renderHook(() => useNumber())
4848
expect(result.current).toBe(1)
4949

@@ -56,13 +56,15 @@ describe("connectFactoryObservable", () => {
5656
it("suspends the component when the observable hasn't emitted yet.", async () => {
5757
const source$ = of(1).pipe(delay(100))
5858
const [useDelayedNumber, getDelayedNumber$] = bind(() => source$)
59-
const subs = getDelayedNumber$().subscribe()
6059
const Result: React.FC = () => <div>Result {useDelayedNumber()}</div>
6160
const TestSuspense: React.FC = () => {
6261
return (
63-
<Suspense fallback={<span>Waiting</span>}>
62+
<Subscribe
63+
source$={getDelayedNumber$()}
64+
fallback={<span>Waiting</span>}
65+
>
6466
<Result />
65-
</Suspense>
67+
</Subscribe>
6668
)
6769
}
6870

@@ -75,7 +77,51 @@ describe("connectFactoryObservable", () => {
7577

7678
expect(screen.queryByText("Result 1")).not.toBeNull()
7779
expect(screen.queryByText("Waiting")).toBeNull()
78-
subs.unsubscribe()
80+
})
81+
82+
it("synchronously mounts the emitted value if the observable emits synchronously", () => {
83+
const source$ = of(1)
84+
const [useDelayedNumber, getDelayedNumber$] = bind(() => source$)
85+
const Result: React.FC = () => <div>Result {useDelayedNumber()}</div>
86+
const TestSuspense: React.FC = () => {
87+
return (
88+
<Subscribe
89+
source$={getDelayedNumber$()}
90+
fallback={<span>Waiting</span>}
91+
>
92+
<Result />
93+
</Subscribe>
94+
)
95+
}
96+
97+
render(<TestSuspense />)
98+
99+
expect(screen.queryByText("Result 1")).not.toBeNull()
100+
expect(screen.queryByText("Waiting")).toBeNull()
101+
})
102+
103+
it("doesn't mount the fallback element if the subscription is already active", () => {
104+
const source$ = new Subject<number>()
105+
const [useDelayedNumber, getDelayedNumber$] = bind(() => source$)
106+
const Result: React.FC = () => <div>Result {useDelayedNumber()}</div>
107+
const TestSuspense: React.FC = () => {
108+
return (
109+
<Subscribe
110+
source$={getDelayedNumber$()}
111+
fallback={<span>Waiting</span>}
112+
>
113+
<Result />
114+
</Subscribe>
115+
)
116+
}
117+
118+
const subscription = getDelayedNumber$().subscribe()
119+
source$.next(1)
120+
render(<TestSuspense />)
121+
122+
expect(screen.queryByText("Result 1")).not.toBeNull()
123+
expect(screen.queryByText("Waiting")).toBeNull()
124+
subscription.unsubscribe()
79125
})
80126

81127
it("shares the multicasted subscription with all of the components that use the same parameters", async () => {
@@ -114,7 +160,12 @@ describe("connectFactoryObservable", () => {
114160
})
115161

116162
it("returns the value of next new Observable when the arguments change", () => {
117-
const [useNumber] = bind((x: number) => of(x))
163+
const [useNumber, getNumber$] = bind((x: number) => of(x))
164+
const subs = merge(
165+
getNumber$(0),
166+
getNumber$(1),
167+
getNumber$(2),
168+
).subscribe()
118169
const { result, rerender } = renderHook(({ input }) => useNumber(input), {
119170
initialProps: { input: 0 },
120171
})
@@ -129,6 +180,7 @@ describe("connectFactoryObservable", () => {
129180
rerender({ input: 2 })
130181
})
131182
expect(result.current).toBe(2)
183+
subs.unsubscribe()
132184
})
133185

134186
it("handles optional args correctly", () => {
@@ -149,9 +201,12 @@ describe("connectFactoryObservable", () => {
149201
const [input, setInput] = useState(0)
150202
return (
151203
<>
152-
<Suspense fallback={<span>Waiting</span>}>
204+
<Subscribe
205+
source$={getDelayedNumber$(input)}
206+
fallback={<span>Waiting</span>}
207+
>
153208
<Result input={input} />
154-
</Suspense>
209+
</Subscribe>
155210
<button onClick={() => setInput((x) => x + 1)}>increase</button>
156211
</>
157212
)
@@ -223,8 +278,8 @@ describe("connectFactoryObservable", () => {
223278
})
224279

225280
it("allows errors to be caught in error boundaries", () => {
226-
const errStream = new BehaviorSubject(1)
227-
const [useError] = bind(() => errStream)
281+
const errStream = new Subject()
282+
const [useError] = bind(() => errStream, 1)
228283

229284
const ErrorComponent = () => {
230285
const value = useError()
@@ -253,7 +308,7 @@ describe("connectFactoryObservable", () => {
253308
const errStream = new Observable((observer) =>
254309
observer.error("controlled error"),
255310
)
256-
const [useError] = bind((_: string) => errStream)
311+
const [useError, getErrStream$] = bind((_: string) => errStream)
257312

258313
const ErrorComponent = () => {
259314
const value = useError("foo")
@@ -264,9 +319,12 @@ describe("connectFactoryObservable", () => {
264319
const errorCallback = jest.fn()
265320
const { unmount } = render(
266321
<TestErrorBoundary onError={errorCallback}>
267-
<Suspense fallback={<div>Loading...</div>}>
322+
<Subscribe
323+
source$={getErrStream$("foo")}
324+
fallback={<div>Loading...</div>}
325+
>
268326
<ErrorComponent />
269-
</Suspense>
327+
</Subscribe>
270328
</TestErrorBoundary>,
271329
)
272330

@@ -279,7 +337,7 @@ describe("connectFactoryObservable", () => {
279337

280338
it("allows async errors to be caught in error boundaries with suspense", async () => {
281339
const errStream = new Subject()
282-
const [useError] = bind((_: string) => errStream)
340+
const [useError, getErrStream$] = bind((_: string) => errStream)
283341

284342
const ErrorComponent = () => {
285343
const value = useError("foo")
@@ -290,9 +348,12 @@ describe("connectFactoryObservable", () => {
290348
const errorCallback = jest.fn()
291349
const { unmount } = render(
292350
<TestErrorBoundary onError={errorCallback}>
293-
<Suspense fallback={<div>Loading...</div>}>
351+
<Subscribe
352+
source$={getErrStream$("foo")}
353+
fallback={<div>Loading...</div>}
354+
>
294355
<ErrorComponent />
295-
</Suspense>
356+
</Subscribe>
296357
</TestErrorBoundary>,
297358
)
298359

@@ -325,19 +386,24 @@ describe("connectFactoryObservable", () => {
325386
.pipe(catchError(() => []))
326387
.subscribe()
327388

389+
const Ok: React.FC<{ ok: boolean }> = ({ ok }) => <>{useOkKo(ok)}</>
390+
328391
const ErrorComponent = () => {
329392
const [ok, setOk] = useState(true)
330-
const value = useOkKo(ok)
331393

332-
return <span onClick={() => setOk(false)}>{value}</span>
394+
return (
395+
<Subscribe source$={getObs$(ok)} fallback={<div>Loading...</div>}>
396+
<span onClick={() => setOk(false)}>
397+
<Ok ok={ok} />
398+
</span>
399+
</Subscribe>
400+
)
333401
}
334402

335403
const errorCallback = jest.fn()
336404
const { unmount } = render(
337405
<TestErrorBoundary onError={errorCallback}>
338-
<Suspense fallback={<div>Loading...</div>}>
339-
<ErrorComponent />
340-
</Suspense>
406+
<ErrorComponent />
341407
</TestErrorBoundary>,
342408
)
343409

@@ -367,12 +433,11 @@ describe("connectFactoryObservable", () => {
367433
)
368434

369435
it("doesn't throw errors on components that will get unmounted on the next cycle", () => {
370-
const valueStream = new BehaviorSubject(1)
371-
const [useValue, value$] = bind(() => valueStream)
372-
const [useError] = bind(() =>
373-
value$().pipe(
374-
switchMap((v) => (v === 1 ? of(v) : throwError("error"))),
375-
),
436+
const valueStream = new Subject<number>()
437+
const [useValue, value$] = bind(() => valueStream, 1)
438+
const [useError] = bind(
439+
() => value$().pipe(switchMapTo(throwError("error"))),
440+
1,
376441
)
377442

378443
const ErrorComponent: FC = () => {
@@ -403,30 +468,6 @@ describe("connectFactoryObservable", () => {
403468
expect(errorCallback).not.toHaveBeenCalled()
404469
})
405470

406-
it("does not resubscribe to an observable that emits synchronously and that does not have a top-level subscription after a re-render", () => {
407-
let nTopSubscriptions = 0
408-
409-
const [useNTopSubscriptions] = bind((id: number) =>
410-
defer(() => {
411-
return of(++nTopSubscriptions + id)
412-
}),
413-
)
414-
415-
const { result, rerender, unmount } = renderHook(() =>
416-
useNTopSubscriptions(0),
417-
)
418-
419-
expect(result.current).toBe(2)
420-
421-
actHook(() => {
422-
rerender()
423-
})
424-
expect(result.current).toBe(2)
425-
expect(nTopSubscriptions).toBe(2)
426-
427-
unmount()
428-
})
429-
430471
it("if the observable hasn't emitted and a defaultValue is provided, it does not start suspense", () => {
431472
const number$ = new Subject<number>()
432473
const [useNumber] = bind(

0 commit comments

Comments
 (0)