diff --git a/README.md b/README.md
index 1ab798b5..854aad8b 100644
--- a/README.md
+++ b/README.md
@@ -396,3 +396,6 @@ const App = () => {
);
};
```
+
+## Known issues
+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.
diff --git a/package.json b/package.json
index a4f3cefd..6a72d3e4 100644
--- a/package.json
+++ b/package.json
@@ -26,10 +26,12 @@
"devDependencies": {
"@types/jest": "^27.0.1",
"@types/lodash": "^4.14.149",
+ "@types/react": "^18.0.12",
"@types/react-dom": "^18.0.0",
"@types/react-test-renderer": "^18.0.0",
"@types/sinon": "^10.0.0",
"@types/sinon-chai": "^3.2.3",
+ "@types/use-sync-external-store": "^0.0.3",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"@vitejs/plugin-react": "^1.3.2",
@@ -43,9 +45,9 @@
"jest": "^27.0.4",
"lint-staged": "^13.0.0",
"prettier": "^2.0.1",
- "react": "17.0.2",
- "react-dom": "17.0.2",
- "react-test-renderer": "17.0.2",
+ "react": "18.1.0",
+ "react-dom": "18.1.0",
+ "react-test-renderer": "18.1.0",
"rxjs": "^7.0.0",
"sinon": "^14.0.0",
"standard": "^17.0.0",
@@ -55,11 +57,12 @@
},
"dependencies": {
"tslib": "^2.1.0",
- "use-constant": "^1.0.0"
+ "use-constant": "^1.0.0",
+ "use-sync-external-store": "^1.1.0"
},
"peerDependencies": {
- "react": "17.0.2",
- "rxjs": "^7.0.0"
+ "react": ">=16.8.0",
+ "rxjs": ">=7.0.0"
},
"lint-staged": {
"*.js": [
diff --git a/playground/index.tsx b/playground/index.tsx
index c9701979..6f967ee2 100644
--- a/playground/index.tsx
+++ b/playground/index.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
-import ReactDOM from 'react-dom'
+import ReactDOM from 'react-dom/client'
import { interval, Observable, timer } from 'rxjs'
import { exhaustMap, map, scan, switchMap } from 'rxjs/operators'
@@ -43,4 +43,4 @@ function App() {
)
}
-ReactDOM.render(, document.querySelector('#app'))
+ReactDOM.createRoot(document.querySelector('#app')!).render()
diff --git a/src/__test__/use-event-callback.spec.tsx b/src/__test__/use-event-callback.spec.tsx
index e8d1ef0b..340e8fb4 100644
--- a/src/__test__/use-event-callback.spec.tsx
+++ b/src/__test__/use-event-callback.spec.tsx
@@ -8,7 +8,7 @@ import { find } from './find'
import { useEventCallback } from '../use-event-callback'
describe('useEventCallback specs', () => {
- function createFixture(
+ function createFixture(
factory: (event$: Observable>) => Observable,
initialValue?: T,
) {
diff --git a/src/use-event-callback.ts b/src/use-event-callback.ts
index 4959eb7d..59653a3a 100644
--- a/src/use-event-callback.ts
+++ b/src/use-event-callback.ts
@@ -1,6 +1,8 @@
-import { useEffect, useState, useCallback } from 'react'
+import { useEffect, useCallback, useMemo } from 'react'
import useConstant from 'use-constant'
import { Observable, BehaviorSubject, Subject } from 'rxjs'
+import { tap } from 'rxjs/operators'
+import { useSyncExternalStore } from 'use-sync-external-store/shim'
import { RestrictArray, VoidAsNull, Not } from './type'
@@ -44,42 +46,46 @@ export function useEventCallback(
inputs?: RestrictArray,
): ReturnedState {
const initialValue = (typeof initialState !== 'undefined' ? initialState : null) as VoidAsNull
- const [state, setState] = useState(initialValue)
const event$ = useConstant(() => new Subject())
const state$ = useConstant(() => new BehaviorSubject(initialValue))
const inputs$ = useConstant(
() => new BehaviorSubject | null>(typeof inputs === 'undefined' ? null : inputs),
)
- function eventCallback(e: EventValue) {
- return event$.next(e)
- }
- const returnedCallback = useCallback(eventCallback, [])
+ useEffect(() => {
+ return () => {
+ state$.complete()
+ inputs$.complete()
+ event$.complete()
+ }
+ }, [])
+
+ const returnedCallback = useCallback(function eventCallback(e: EventValue) {
+ event$.next(e)
+ }, [])
useEffect(() => {
inputs$.next(inputs!)
}, inputs || [])
- useEffect(() => {
- setState(initialValue)
+ const subscribe = useMemo(() => {
let value$: Observable
-
if (!inputs) {
value$ = (callback as EventCallback)(event$, state$ as Observable)
} else {
value$ = (callback as any)(event$, state$ as Observable, inputs$ as Observable)
}
- const subscription = value$.subscribe((value) => {
- state$.next(value)
- setState(value as VoidAsNull)
- })
- return () => {
- subscription.unsubscribe()
- state$.complete()
- inputs$.complete()
- event$.complete()
+ return (onStorageChange: () => void) => {
+ const subscription = value$.pipe(tap((s) => state$.next(s))).subscribe(onStorageChange)
+ return () => subscription.unsubscribe()
}
- }, []) // immutable forever
+ }, [])
+
+ const getSnapShot = useMemo(() => {
+ return () => state$.getValue() as VoidAsNull
+ }, [])
+
+ const state = useSyncExternalStore(subscribe, getSnapShot, getSnapShot)
return [returnedCallback as VoidableEventCallback, state]
}
diff --git a/src/use-observable.ts b/src/use-observable.ts
index 76826f50..f3dd872c 100644
--- a/src/use-observable.ts
+++ b/src/use-observable.ts
@@ -1,6 +1,8 @@
import { Observable, BehaviorSubject } from 'rxjs'
-import { useState, useEffect } from 'react'
+import { tap } from 'rxjs/operators'
+import { useEffect, useMemo } from 'react'
import useConstant from 'use-constant'
+import { useSyncExternalStore } from 'use-sync-external-store/shim'
import { RestrictArray } from './type'
@@ -22,39 +24,41 @@ export function useObservable>(
initialState?: State,
inputs?: RestrictArray,
): State | null {
- const [state, setState] = useState(typeof initialState !== 'undefined' ? initialState : null)
-
const state$ = useConstant(() => new BehaviorSubject(initialState))
const inputs$ = useConstant(() => new BehaviorSubject | undefined>(inputs))
+ useEffect(() => {
+ return () => {
+ state$.complete()
+ inputs$.complete()
+ }
+ }, [])
+
useEffect(() => {
inputs$.next(inputs)
}, inputs || [])
- useEffect(() => {
- let output$: BehaviorSubject
+ const subscribe = useMemo(() => {
+ let output$: Observable
if (inputs) {
output$ = (
inputFactory as (
state$: Observable,
inputs$: Observable | undefined>,
) => Observable
- )(state$, inputs$) as BehaviorSubject
+ )(state$, inputs$)
} else {
- output$ = (inputFactory as unknown as (state$: Observable) => Observable)(
- state$,
- ) as BehaviorSubject
+ output$ = (inputFactory as unknown as (state$: Observable) => Observable)(state$)
}
- const subscription = output$.subscribe((value) => {
- state$.next(value)
- setState(value)
- })
- return () => {
- subscription.unsubscribe()
- inputs$.complete()
- state$.complete()
+ return (onStorageChange: () => void) => {
+ const subscription = output$.pipe(tap((s) => state$.next(s))).subscribe(onStorageChange)
+ return () => subscription.unsubscribe()
}
- }, []) // immutable forever
+ }, [])
+
+ const getSnapShot = useMemo(() => {
+ return () => state$.getValue() ?? null
+ }, [])
- return state
+ return useSyncExternalStore(subscribe, getSnapShot, getSnapShot)
}
diff --git a/yarn.lock b/yarn.lock
index 183f1c16..f02ee2d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -800,10 +800,10 @@
dependencies:
"@types/react" "*"
-"@types/react@*":
- version "17.0.3"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
- integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
+"@types/react@*", "@types/react@^18.0.12":
+ version "18.0.12"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.12.tgz#cdaa209d0a542b3fcf69cf31a03976ec4cdd8840"
+ integrity sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -839,6 +839,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==
+"@types/use-sync-external-store@^0.0.3":
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
+ integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
+
"@types/yargs-parser@*":
version "20.2.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
@@ -3911,55 +3916,57 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-react-dom@17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
- integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
+react-dom@18.1.0:
+ version "18.1.0"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f"
+ integrity sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==
dependencies:
loose-envify "^1.1.0"
- object-assign "^4.1.1"
- scheduler "^0.20.2"
+ scheduler "^0.22.0"
-"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
- integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.1.0:
+ version "18.1.0"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
+ integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-is@^17.0.1:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
react-refresh@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.13.0.tgz#cbd01a4482a177a5da8d44c9755ebb1f26d5a1c1"
integrity sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==
-react-shallow-renderer@^16.13.1:
- version "16.14.1"
- resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124"
- integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==
+react-shallow-renderer@^16.15.0:
+ version "16.15.0"
+ resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457"
+ integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==
dependencies:
object-assign "^4.1.1"
- react-is "^16.12.0 || ^17.0.0"
+ react-is "^16.12.0 || ^17.0.0 || ^18.0.0"
-react-test-renderer@17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
- integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==
+react-test-renderer@18.1.0:
+ version "18.1.0"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.1.0.tgz#35b75754834cf9ab517b6813db94aee0a6b545c3"
+ integrity sha512-OfuueprJFW7h69GN+kr4Ywin7stcuqaYAt1g7airM5cUgP0BoF5G5CXsPGmXeDeEkncb2fqYNECO4y18sSqphg==
dependencies:
- object-assign "^4.1.1"
- react-is "^17.0.2"
- react-shallow-renderer "^16.13.1"
- scheduler "^0.20.2"
+ react-is "^18.1.0"
+ react-shallow-renderer "^16.15.0"
+ scheduler "^0.22.0"
-react@17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
- integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
+react@18.1.0:
+ version "18.1.0"
+ resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
+ integrity sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==
dependencies:
loose-envify "^1.1.0"
- object-assign "^4.1.1"
regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
version "1.4.3"
@@ -4087,13 +4094,12 @@ saxes@^5.0.1:
dependencies:
xmlchars "^2.2.0"
-scheduler@^0.20.2:
- version "0.20.2"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
- integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
+scheduler@^0.22.0:
+ version "0.22.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.22.0.tgz#83a5d63594edf074add9a7198b1bae76c3db01b8"
+ integrity sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==
dependencies:
loose-envify "^1.1.0"
- object-assign "^4.1.1"
semver@7.x, semver@^7.0.0, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5:
version "7.3.7"
@@ -4612,6 +4618,11 @@ use-constant@^1.0.0:
resolved "https://registry.yarnpkg.com/use-constant/-/use-constant-1.1.0.tgz#76d36a0edf16d4cc8565361f522b55da5f8f3f22"
integrity sha512-yrflEfv7Xv/W8WlYV6nwRH01K+2BpR4cWxuzY03yPRjYZuHixhGlvnJN5O2bRYrXGpJ4zy8QjFABGIQ2QXeBOA==
+use-sync-external-store@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82"
+ integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ==
+
uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"