Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-rx

Package Overview
Dependencies
Maintainers
62
Versions
73
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-rx - npm Package Compare versions

Comparing version 3.1.3 to 4.0.0-canary.0

2

dist/index.d.ts

@@ -18,3 +18,3 @@ import {Observable} from 'rxjs'

observable: ObservableType,
initialValue: InitialValue,
initialValue: InitialValue | (() => InitialValue),
): InitialValue | ObservedValueOf<ObservableType>

@@ -21,0 +21,0 @@

@@ -1,3 +0,3 @@

import { useRef, useEffect, useMemo, useSyncExternalStore, useState } from "react";
import { catchError, of, finalize, share } from "rxjs";
import { useMemo, useSyncExternalStore, useState, useEffect } from "react";
import { catchError, of, finalize, share, timer, asapScheduler } from "rxjs";
import { map, tap } from "rxjs/operators";

@@ -11,31 +11,27 @@ import { observableCallback } from "observable-callback";

function useObservable(observable, initialValue) {
const initialValueRef = useRef(getValue(initialValue));
useEffect(() => {
initialValueRef.current = getValue(initialValue);
}, [initialValue]);
if (!cache.has(observable)) {
const entry = {
snapshot: getValue(initialValue)
};
entry.observable = observable.pipe(
map((value) => ({ snapshot: value, error: void 0 })),
catchError((error) => of({ snapshot: void 0, error })),
tap(({ snapshot, error }) => {
entry.snapshot = snapshot, entry.error = error;
}),
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
map((value) => {
}),
// Ensure that the cache entry is deleted when the observable completes or errors.
finalize(() => cache.delete(observable)),
share({ resetOnRefCountZero: () => timer(0, asapScheduler) })
), entry.observable.subscribe().unsubscribe(), cache.set(observable, entry);
}
const store = useMemo(() => {
if (!cache.has(observable)) {
const entry = {
snapshot: initialValueRef.current
};
entry.observable = observable.pipe(
map((value) => ({ snapshot: value, error: void 0 })),
catchError((error) => of({ snapshot: void 0, error })),
tap(({ snapshot, error }) => {
entry.snapshot = snapshot, entry.error = error;
}),
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
map((value) => {
}),
// Ensure that the cache entry is deleted when the observable completes or errors.
finalize(() => cache.delete(observable)),
share()
), entry.subscription = entry.observable.subscribe(), cache.set(observable, entry);
}
const instance = cache.get(observable);
return instance.subscription.closed && (instance.subscription = instance.observable.subscribe()), {
return {
subscribe: (onStoreChange) => {
const subscription = instance.observable.subscribe(onStoreChange);
return instance.subscription.unsubscribe(), () => {
return () => {
subscription.unsubscribe();

@@ -54,3 +50,3 @@ };

store.getSnapshot,
typeof initialValueRef.current > "u" ? void 0 : () => initialValueRef.current
typeof initialValue > "u" ? void 0 : () => getValue(initialValue)
);

@@ -57,0 +53,0 @@ }

{
"name": "react-rx",
"version": "3.1.3",
"version": "4.0.0-canary.0",
"description": "React + RxJS = <3",

@@ -65,11 +65,2 @@ "keywords": [

],
"scripts": {
"build": "pkg build --strict --clean --check",
"dev": "pnpm --filter 'react-rx-website' dev",
"format": "prettier --cache --write .",
"lint": "eslint --cache .",
"prepublishOnly": "pnpm build",
"test": "vitest run --typecheck",
"watch": "pnpm build -- --watch"
},
"browserslist": "extends @sanity/browserslist-config",

@@ -82,21 +73,21 @@ "prettier": "@sanity/prettier-config",

"devDependencies": {
"@sanity/pkg-utils": "^6.10.3",
"@sanity/prettier-config": "^1.0.2",
"@sanity/pkg-utils": "^6.11.7",
"@sanity/prettier-config": "^1.0.3",
"@sanity/semantic-release-preset": "^5.0.0",
"@testing-library/dom": "^10.3.1",
"@testing-library/react": "^16.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.1",
"@types/node": "^18.17.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"eslint": "^8.57.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-compiler": "0.0.0-experimental-51a85ea-20240601",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-compiler": "beta",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"jsdom": "^24.1.0",
"prettier": "^3.3.2",
"prettier": "^3.3.3",
"react": "^18.3.1",

@@ -106,5 +97,5 @@ "react-dom": "^18.3.1",

"rxjs": "^7.8.1",
"semantic-release": "^24.0.0",
"typescript": "5.5.3",
"vitest": "^2.0.2"
"semantic-release": "^24.1.0",
"typescript": "5.6.3",
"vitest": "^2.1.4"
},

@@ -115,3 +106,10 @@ "peerDependencies": {

},
"packageManager": "pnpm@9.5.0"
}
"scripts": {
"build": "pkg build --strict --clean --check",
"dev": "pnpm --filter 'react-rx-website' dev",
"format": "prettier --cache --write .",
"lint": "eslint --cache .",
"test": "vitest run --typecheck",
"watch": "pnpm build -- --watch"
}
}
import {act, render} from '@testing-library/react'
import {createElement, Fragment, StrictMode, useEffect} from 'react'
import {createElement, Fragment, StrictMode, useEffect, useMemo} from 'react'
import {BehaviorSubject, Observable} from 'rxjs'

@@ -42,3 +42,3 @@ import {expect, test} from 'vitest'

test('Strict mode should unsubscribe the source observable on unmount', () => {
test('Strict mode should unsubscribe the source observable on unmount', async () => {
const subscribed: number[] = []

@@ -61,5 +61,29 @@ const unsubscribed: number[] = []

const {rerender} = render(createElement(StrictMode, null, createElement(ObservableComponent)))
expect(subscribed).toEqual([0, 1])
expect(subscribed).toEqual([0])
rerender(createElement(StrictMode, null, createElement('div')))
expect(unsubscribed).toEqual([0, 1])
await Promise.resolve()
expect(unsubscribed).toEqual([0])
})
test('Strict mode should unsubscribe the source observable on unmount if its created in a useMemo', async () => {
let subscriberCount: number = 0
const getObservable = () =>
new Observable(() => {
subscriberCount++
return () => {
subscriberCount--
}
})
function ObservableComponent() {
const memoObservable = useMemo(() => getObservable(), [])
useObservable(memoObservable)
return createElement(Fragment, null)
}
const {rerender} = render(createElement(StrictMode, null, createElement(ObservableComponent)))
expect(subscriberCount, 'Subscriber count should be 2').toBe(2)
rerender(createElement(StrictMode, null, createElement('div')))
await Promise.resolve()
expect(subscriberCount, 'Subscriber count should be 0').toBe(0)
})

@@ -25,4 +25,4 @@ import {of} from 'rxjs'

expectTypeOf(useObservable(observable, 1)).toEqualTypeOf<string | number>()
expectTypeOf(useObservable(observable, () => 1)).toEqualTypeOf<string | number>()
expectTypeOf(useObservable(observable, 'foo')).toEqualTypeOf<string>()
})

@@ -9,3 +9,3 @@ import {act, render, renderHook} from '@testing-library/react'

test('should subscribe immediately on component mount and unsubscribe on component unmount', () => {
test('should subscribe immediately on component mount and unsubscribe on component unmount', async () => {
let subscribed = false

@@ -25,6 +25,7 @@ const observable = new Observable(() => {

unmount()
await Promise.resolve()
expect(subscribed).toBe(false)
})
test('should only subscribe once when given same observable on re-renders', () => {
test('should only subscribe once when given same observable on re-renders', async () => {
let subscriptionCount = 0

@@ -42,2 +43,3 @@ const observable = new Observable(() => {

unmount()
await Promise.resolve()

@@ -109,3 +111,3 @@ renderHook(() => useObservable(observable))

test('should rerender with initial value if component unmounts and then remounts', () => {
test('should rerender with initial value if component unmounts and then remounts', async () => {
const values$ = new Subject<string>()

@@ -120,2 +122,3 @@ const firstHook = renderHook(() => useObservable(values$, 'initial'))

firstHook.unmount()
await Promise.resolve()

@@ -127,3 +130,3 @@ const nextHook = renderHook(() => useObservable(values$, 'initial2'))

test('should share the observable between each concurrent subscribing hook', () => {
test('should share the observable between each concurrent subscribing hook', async () => {
let subscribeCount = 0

@@ -139,2 +142,3 @@ const observable = new Observable<number>((subscriber) => {

secondHook.unmount()
await Promise.resolve()

@@ -146,3 +150,3 @@ const thirdHook = renderHook(() => useObservable(observable))

test('should restart any completed observable on mount', () => {
test('should restart any completed observable on mount', async () => {
let subscribeCount = 0

@@ -189,2 +193,3 @@ let unsubscribeCount = 0

firstHook.unmount()
await Promise.resolve()

@@ -196,2 +201,4 @@ const secondHook = renderHook(() => useObservable(observable))

secondHook.unmount()
await Promise.resolve()
expect(unsubscribeCount).toBe(2)

@@ -198,0 +205,0 @@ })

@@ -1,3 +0,4 @@

import {useEffect, useMemo, useRef, useSyncExternalStore} from 'react'
import {useMemo, useSyncExternalStore} from 'react'
import {
asapScheduler,
catchError,

@@ -9,3 +10,3 @@ finalize,

share,
type Subscription,
timer,
} from 'rxjs'

@@ -19,3 +20,2 @@ import {map, tap} from 'rxjs/operators'

interface CacheRecord<T> {
subscription: Subscription
observable: Observable<void>

@@ -40,3 +40,3 @@ snapshot: T

observable: ObservableType,
initialValue: InitialValue,
initialValue: InitialValue | (() => InitialValue),
): InitialValue | ObservedValueOf<ObservableType>

@@ -48,50 +48,34 @@ /** @public */

): InitialValue | ObservedValueOf<ObservableType> {
/**
* Store the initialValue in a ref, as we don't want a changed `initialValue` to trigger a re-subscription.
* But we also don't want the initialValue to be stale if the observable changes.
*/
const initialValueRef = useRef(getValue(initialValue) as ObservedValueOf<ObservableType>)
if (!cache.has(observable)) {
const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {
snapshot: getValue(initialValue) as ObservedValueOf<ObservableType>,
}
entry.observable = observable.pipe(
map((value) => ({snapshot: value, error: undefined})),
catchError((error) => of({snapshot: undefined, error})),
tap(({snapshot, error}) => {
entry.snapshot = snapshot
entry.error = error
}),
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
map((value) => void value),
// Ensure that the cache entry is deleted when the observable completes or errors.
finalize(() => cache.delete(observable)),
share({resetOnRefCountZero: () => timer(0, asapScheduler)}),
)
/**
* Ensures that the initialValue is always up-to-date in case the observable changes.
*/
useEffect(() => {
initialValueRef.current = getValue(initialValue) as ObservedValueOf<ObservableType>
}, [initialValue])
// Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
const subscription = entry.observable.subscribe()
subscription.unsubscribe()
cache.set(observable, entry as CacheRecord<ObservedValueOf<ObservableType>>)
}
const store = useMemo(() => {
if (!cache.has(observable)) {
const entry: Partial<CacheRecord<ObservedValueOf<ObservableType>>> = {
snapshot: initialValueRef.current,
}
entry.observable = observable.pipe(
map((value) => ({snapshot: value, error: undefined})),
catchError((error) => of({snapshot: undefined, error})),
tap(({snapshot, error}) => {
entry.snapshot = snapshot
entry.error = error
}),
// Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state
// and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type.
map((value) => void value),
// Ensure that the cache entry is deleted when the observable completes or errors.
finalize(() => cache.delete(observable)),
share(),
)
// Eagerly subscribe to sync set `entry.currentValue` to what the observable returns, and keep the observable alive until the component unmounts.
entry.subscription = entry.observable.subscribe()
cache.set(observable, entry as CacheRecord<ObservedValueOf<ObservableType>>)
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const instance = cache.get(observable)!
if (instance.subscription.closed) {
instance.subscription = instance.observable.subscribe()
}
return {
subscribe: (onStoreChange: () => void) => {
const subscription = instance.observable.subscribe(onStoreChange)
instance.subscription.unsubscribe()
return () => {

@@ -113,4 +97,6 @@ subscription.unsubscribe()

store.getSnapshot,
typeof initialValueRef.current === 'undefined' ? undefined : () => initialValueRef.current,
typeof initialValue === 'undefined'
? undefined
: () => getValue(initialValue) as ObservedValueOf<ObservableType>,
)
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc