Skip to content

Commit c3b2ec7

Browse files
authored
Merge pull request #1020 from InfiniteXyy/feat/react-18
support react 18
2 parents ea4aca7 + 5c4a0ee commit c3b2ec7

File tree

7 files changed

+111
-84
lines changed

7 files changed

+111
-84
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,6 @@ const App = () => {
396396
);
397397
};
398398
```
399+
400+
## Known issues
401+
If you are using React 18 + `StrictMode`, `rxjs-hooks` will not work properly. Because in React 18, `StrictMode` will force unmount hooks to trigger twice, which will result in unexpected behaviours.

package.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
"devDependencies": {
2727
"@types/jest": "^27.0.1",
2828
"@types/lodash": "^4.14.149",
29+
"@types/react": "^18.0.12",
2930
"@types/react-dom": "^18.0.0",
3031
"@types/react-test-renderer": "^18.0.0",
3132
"@types/sinon": "^10.0.0",
3233
"@types/sinon-chai": "^3.2.3",
34+
"@types/use-sync-external-store": "^0.0.3",
3335
"@typescript-eslint/eslint-plugin": "^4.19.0",
3436
"@typescript-eslint/parser": "^4.19.0",
3537
"@vitejs/plugin-react": "^1.3.2",
@@ -43,9 +45,9 @@
4345
"jest": "^27.0.4",
4446
"lint-staged": "^13.0.0",
4547
"prettier": "^2.0.1",
46-
"react": "17.0.2",
47-
"react-dom": "17.0.2",
48-
"react-test-renderer": "17.0.2",
48+
"react": "18.1.0",
49+
"react-dom": "18.1.0",
50+
"react-test-renderer": "18.1.0",
4951
"rxjs": "^7.0.0",
5052
"sinon": "^14.0.0",
5153
"standard": "^17.0.0",
@@ -55,11 +57,12 @@
5557
},
5658
"dependencies": {
5759
"tslib": "^2.1.0",
58-
"use-constant": "^1.0.0"
60+
"use-constant": "^1.0.0",
61+
"use-sync-external-store": "^1.1.0"
5962
},
6063
"peerDependencies": {
61-
"react": "17.0.2",
62-
"rxjs": "^7.0.0"
64+
"react": ">=16.8.0",
65+
"rxjs": ">=7.0.0"
6366
},
6467
"lint-staged": {
6568
"*.js": [

playground/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState } from 'react'
2-
import ReactDOM from 'react-dom'
2+
import ReactDOM from 'react-dom/client'
33
import { interval, Observable, timer } from 'rxjs'
44
import { exhaustMap, map, scan, switchMap } from 'rxjs/operators'
55

@@ -43,4 +43,4 @@ function App() {
4343
)
4444
}
4545

46-
ReactDOM.render(<App />, document.querySelector('#app'))
46+
ReactDOM.createRoot(document.querySelector('#app')!).render(<App />)

src/__test__/use-event-callback.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { find } from './find'
88
import { useEventCallback } from '../use-event-callback'
99

1010
describe('useEventCallback specs', () => {
11-
function createFixture<T>(
11+
function createFixture<T extends React.ReactNode>(
1212
factory: (event$: Observable<React.SyntheticEvent<any>>) => Observable<T>,
1313
initialValue?: T,
1414
) {

src/use-event-callback.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { useEffect, useState, useCallback } from 'react'
1+
import { useEffect, useCallback, useMemo } from 'react'
22
import useConstant from 'use-constant'
33
import { Observable, BehaviorSubject, Subject } from 'rxjs'
4+
import { tap } from 'rxjs/operators'
5+
import { useSyncExternalStore } from 'use-sync-external-store/shim'
46

57
import { RestrictArray, VoidAsNull, Not } from './type'
68

@@ -44,42 +46,46 @@ export function useEventCallback<EventValue, State = void, Inputs = void>(
4446
inputs?: RestrictArray<Inputs>,
4547
): ReturnedState<EventValue, State | null, Inputs> {
4648
const initialValue = (typeof initialState !== 'undefined' ? initialState : null) as VoidAsNull<State>
47-
const [state, setState] = useState(initialValue)
4849
const event$ = useConstant(() => new Subject<EventValue>())
4950
const state$ = useConstant(() => new BehaviorSubject<State | null>(initialValue))
5051
const inputs$ = useConstant(
5152
() => new BehaviorSubject<RestrictArray<Inputs> | null>(typeof inputs === 'undefined' ? null : inputs),
5253
)
5354

54-
function eventCallback(e: EventValue) {
55-
return event$.next(e)
56-
}
57-
const returnedCallback = useCallback(eventCallback, [])
55+
useEffect(() => {
56+
return () => {
57+
state$.complete()
58+
inputs$.complete()
59+
event$.complete()
60+
}
61+
}, [])
62+
63+
const returnedCallback = useCallback(function eventCallback(e: EventValue) {
64+
event$.next(e)
65+
}, [])
5866

5967
useEffect(() => {
6068
inputs$.next(inputs!)
6169
}, inputs || [])
6270

63-
useEffect(() => {
64-
setState(initialValue)
71+
const subscribe = useMemo(() => {
6572
let value$: Observable<State>
66-
6773
if (!inputs) {
6874
value$ = (callback as EventCallback<EventValue, State, void>)(event$, state$ as Observable<State>)
6975
} else {
7076
value$ = (callback as any)(event$, state$ as Observable<State>, inputs$ as Observable<Inputs>)
7177
}
72-
const subscription = value$.subscribe((value) => {
73-
state$.next(value)
74-
setState(value as VoidAsNull<State>)
75-
})
76-
return () => {
77-
subscription.unsubscribe()
78-
state$.complete()
79-
inputs$.complete()
80-
event$.complete()
78+
return (onStorageChange: () => void) => {
79+
const subscription = value$.pipe(tap((s) => state$.next(s))).subscribe(onStorageChange)
80+
return () => subscription.unsubscribe()
8181
}
82-
}, []) // immutable forever
82+
}, [])
83+
84+
const getSnapShot = useMemo(() => {
85+
return () => state$.getValue() as VoidAsNull<State>
86+
}, [])
87+
88+
const state = useSyncExternalStore(subscribe, getSnapShot, getSnapShot)
8389

8490
return [returnedCallback as VoidableEventCallback<EventValue>, state]
8591
}

src/use-observable.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Observable, BehaviorSubject } from 'rxjs'
2-
import { useState, useEffect } from 'react'
2+
import { tap } from 'rxjs/operators'
3+
import { useEffect, useMemo } from 'react'
34
import useConstant from 'use-constant'
5+
import { useSyncExternalStore } from 'use-sync-external-store/shim'
46

57
import { RestrictArray } from './type'
68

@@ -22,39 +24,41 @@ export function useObservable<State, Inputs extends ReadonlyArray<any>>(
2224
initialState?: State,
2325
inputs?: RestrictArray<Inputs>,
2426
): State | null {
25-
const [state, setState] = useState(typeof initialState !== 'undefined' ? initialState : null)
26-
2727
const state$ = useConstant(() => new BehaviorSubject<State | undefined>(initialState))
2828
const inputs$ = useConstant(() => new BehaviorSubject<RestrictArray<Inputs> | undefined>(inputs))
2929

30+
useEffect(() => {
31+
return () => {
32+
state$.complete()
33+
inputs$.complete()
34+
}
35+
}, [])
36+
3037
useEffect(() => {
3138
inputs$.next(inputs)
3239
}, inputs || [])
3340

34-
useEffect(() => {
35-
let output$: BehaviorSubject<State>
41+
const subscribe = useMemo(() => {
42+
let output$: Observable<State>
3643
if (inputs) {
3744
output$ = (
3845
inputFactory as (
3946
state$: Observable<State | undefined>,
4047
inputs$: Observable<RestrictArray<Inputs> | undefined>,
4148
) => Observable<State>
42-
)(state$, inputs$) as BehaviorSubject<State>
49+
)(state$, inputs$)
4350
} else {
44-
output$ = (inputFactory as unknown as (state$: Observable<State | undefined>) => Observable<State>)(
45-
state$,
46-
) as BehaviorSubject<State>
51+
output$ = (inputFactory as unknown as (state$: Observable<State | undefined>) => Observable<State>)(state$)
4752
}
48-
const subscription = output$.subscribe((value) => {
49-
state$.next(value)
50-
setState(value)
51-
})
52-
return () => {
53-
subscription.unsubscribe()
54-
inputs$.complete()
55-
state$.complete()
53+
return (onStorageChange: () => void) => {
54+
const subscription = output$.pipe(tap((s) => state$.next(s))).subscribe(onStorageChange)
55+
return () => subscription.unsubscribe()
5656
}
57-
}, []) // immutable forever
57+
}, [])
58+
59+
const getSnapShot = useMemo(() => {
60+
return () => state$.getValue() ?? null
61+
}, [])
5862

59-
return state
63+
return useSyncExternalStore(subscribe, getSnapShot, getSnapShot)
6064
}

yarn.lock

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -800,10 +800,10 @@
800800
dependencies:
801801
"@types/react" "*"
802802

803-
"@types/react@*":
804-
version "17.0.3"
805-
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
806-
integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
803+
"@types/react@*", "@types/react@^18.0.12":
804+
version "18.0.12"
805+
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.12.tgz#cdaa209d0a542b3fcf69cf31a03976ec4cdd8840"
806+
integrity sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==
807807
dependencies:
808808
"@types/prop-types" "*"
809809
"@types/scheduler" "*"
@@ -839,6 +839,11 @@
839839
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
840840
integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==
841841

842+
"@types/use-sync-external-store@^0.0.3":
843+
version "0.0.3"
844+
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
845+
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
846+
842847
"@types/yargs-parser@*":
843848
version "20.2.0"
844849
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
@@ -3911,55 +3916,57 @@ queue-microtask@^1.2.2:
39113916
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
39123917
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
39133918

3914-
react-dom@17.0.2:
3915-
version "17.0.2"
3916-
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
3917-
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
3919+
react-dom@18.1.0:
3920+
version "18.1.0"
3921+
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f"
3922+
integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==
39183923
dependencies:
39193924
loose-envify "^1.1.0"
3920-
object-assign "^4.1.1"
3921-
scheduler "^0.20.2"
3925+
scheduler "^0.22.0"
39223926

3923-
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
3924-
version "17.0.2"
3925-
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
3926-
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
3927+
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.1.0:
3928+
version "18.1.0"
3929+
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
3930+
integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==
39273931

39283932
react-is@^16.13.1:
39293933
version "16.13.1"
39303934
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
39313935
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
39323936

3937+
react-is@^17.0.1:
3938+
version "17.0.2"
3939+
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
3940+
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
3941+
39333942
react-refresh@^0.13.0:
39343943
version "0.13.0"
39353944
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.13.0.tgz#cbd01a4482a177a5da8d44c9755ebb1f26d5a1c1"
39363945
integrity sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==
39373946

3938-
react-shallow-renderer@^16.13.1:
3939-
version "16.14.1"
3940-
resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124"
3941-
integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==
3947+
react-shallow-renderer@^16.15.0:
3948+
version "16.15.0"
3949+
resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457"
3950+
integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==
39423951
dependencies:
39433952
object-assign "^4.1.1"
3944-
react-is "^16.12.0 || ^17.0.0"
3953+
react-is "^16.12.0 || ^17.0.0 || ^18.0.0"
39453954

3946-
react-test-renderer@17.0.2:
3947-
version "17.0.2"
3948-
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
3949-
integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==
3955+
react-test-renderer@18.1.0:
3956+
version "18.1.0"
3957+
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.1.0.tgz#35b75754834cf9ab517b6813db94aee0a6b545c3"
3958+
integrity sha512-OfuueprJFW7h69GN+kr4Ywin7stcuqaYAt1g7airM5cUgP0BoF5G5CXsPGmXeDeEkncb2fqYNECO4y18sSqphg==
39503959
dependencies:
3951-
object-assign "^4.1.1"
3952-
react-is "^17.0.2"
3953-
react-shallow-renderer "^16.13.1"
3954-
scheduler "^0.20.2"
3960+
react-is "^18.1.0"
3961+
react-shallow-renderer "^16.15.0"
3962+
scheduler "^0.22.0"
39553963

3956-
react@17.0.2:
3957-
version "17.0.2"
3958-
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
3959-
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
3964+
react@18.1.0:
3965+
version "18.1.0"
3966+
resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
3967+
integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==
39603968
dependencies:
39613969
loose-envify "^1.1.0"
3962-
object-assign "^4.1.1"
39633970

39643971
regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
39653972
version "1.4.3"
@@ -4087,13 +4094,12 @@ saxes@^5.0.1:
40874094
dependencies:
40884095
xmlchars "^2.2.0"
40894096

4090-
scheduler@^0.20.2:
4091-
version "0.20.2"
4092-
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
4093-
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
4097+
scheduler@^0.22.0:
4098+
version "0.22.0"
4099+
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8"
4100+
integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==
40944101
dependencies:
40954102
loose-envify "^1.1.0"
4096-
object-assign "^4.1.1"
40974103

40984104
[email protected], semver@^7.0.0, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5:
40994105
version "7.3.7"
@@ -4612,6 +4618,11 @@ use-constant@^1.0.0:
46124618
resolved "https://registry.yarnpkg.com/use-constant/-/use-constant-1.1.0.tgz#76d36a0edf16d4cc8565361f522b55da5f8f3f22"
46134619
integrity sha512-yrflEfv7Xv/W8WlYV6nwRH01K+2BpR4cWxuzY03yPRjYZuHixhGlvnJN5O2bRYrXGpJ4zy8QjFABGIQ2QXeBOA==
46144620

4621+
use-sync-external-store@^1.1.0:
4622+
version "1.1.0"
4623+
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82"
4624+
integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==
4625+
46154626
uuid@^8.0.0:
46164627
version "8.3.2"
46174628
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"

0 commit comments

Comments
 (0)