npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ns3/rx-state

v5.0.0

Published

Platform agnostic solution for state management built as an extension on RxJs

Downloads

286

Readme

@ns3/rx-state

Reactive State is a platform agnostic solution for state management built as an extension on RxJs. The goal of this solution is to provide standard data structures but with reactive capabilities, as well as configurable way to cache data with minimal effort.

Watch this video the get an idea what Reactive State is about:

Reactive State

Data Structures

BehaviorSubject

The smallest unit and a building block of RxState is BehaviorSubject from RxJs itself.

It serves as your reactive variable, so you can store and update simple values.

import { BehaviorSubject } from 'rxjs';

const currentQuery$ = new BehaviorSubject({ skip: 0, limit: 10 }); // define your data

currentQuery$.subscribe(console.log); // react to your data

currentQuery$.next({ skip: 10, limit: 20 }); // update your data

ConnectSubject

Extension of BehaviorSubject with connect capabilities.

import { ConnectSubject } from '@ns3/rx-state';
import { BehaviorSubject } from 'rxjs';
import { ajax } from 'rxjs/ajax';

const authenticatedUserFetching$ = new BehaviorSubject(false);
const authenticatedUser$ = new ConnectSubject({
  connecting$: authenticatedUserFetching$, // optional
  timeout: 5 * 60 * 1000, // 5 minutes
  strategy: 'lazy',
});

authenticatedUser$.connect$(() => ajax('http://link-to-my-endpoint')).subscribe(console.log);

authenticatedUser$.connect$ - will return state data and use provided function to retrieve data if none is present or is outdated.

authenticatedUserFetching$ - will keep information if connect$ method is at the moment using provided function to retrieve data.

To learn more about connect$ capabilities read Connect.

RxSet

Reactive Set is a data structure that resembles regular JS Set but with reactive capabilities.

import { RxSet } from '@ns3/rx-state';

const fetching = new RxSet<string>();

fetching.has$('id-of-my-entity').subscribe(console.log); // listen to given entity fetching
fetching.keys$().subscribe(console.log) // listen to keys of all fetching entities

fetching.add('id-of-my-entity'); // mark entity as being fetched
fetching.delete('id-of-my-entity'); // mark entity as not being fetched

RxMap

Reactive Map is a data structure that resembles regular JS Map but with reactive capabilities.

import { RxMap } from '@ns3/rx-state';

interface Product {
  id: string;
  price: number;
}

const products = new RxMap<string, Product>();

products.get$('id-of-my-entity').subscribe(console.log); // listen to a product with given key
products.values$().subscribe(console.log) // listen to all products in collection

products.set('id-of-my-entity', { id: 'id-of-my-entity', price 999 }); // set product
products.delete('id-of-my-entity'); // delete product

RxConnectMap

Extension of RxMap with connect capabilities.

import { RxConnectMap, RxSet } from '@ns3/rx-state';

interface Product {
  id: string;
  price: number;
}

const fetching = new RxSet<string>();
const products = new RxConnectMap<string, Product>(
  connectingSet: fetching, // optional
  timeout: 5 * 60 * 1000, // 5 minutes
  scope: 'single',
  strategy: 'lazy',
);

products.connect$(
  'id-of-my-entity',
  () => ajax('http://link-to-my-endpoint/id-of-my-entity'
)).subscribe(console.log);

products.connect$ - will return state data for given key and use provided function to retrieve data if none is present or is outdated.

fetching - will keep information if connect$ method is at the moment using provided function to retrieve data for given key.

To learn more about connect$ capabilities read Connect.

RxArrays

Reactive Arrays is a data structure for keeping arrays of data. It is meant to be used alongside RxMap. RxMap keeps information about entities, RxArrays keeps information which items and in what order are stored.

import { RxArrays, RxMap } from '@ns3/rx-state';

interface Product {
  id: string;
  price: number;
}

const products = new RxMap<string, Product>();
const paginated = new RxArrays({
  itemsMap: products,
  itemKey: (value) => value.id,
});

const itemsToSet: Product[] = [
  { id: 'a', price: 10 },
  { id: 'b', price: 20 },
]

paginated.set('page-1', itemsToSet);

products.get$('a').subscribe(console.log);
paginated.get$('page1').subscribe(console.log);

In the constructor you provide:

  • itemsMap which is a correlated RxMap
  • itemKey which is a function to retrieve a key of a given value to set in RxMap
    • In our example paginated will set { id: 'a', price: 10 } on products RxMap with key 'a', because itemKey: (value) => value.id

products.get$('a') - will return item with id 'a'.

paginated.get$('page-1') - will return itemsToSet.

Any change to items in products RxMap or paginated RxArrays will be reflected in both collections.

RxConnectArrays

Extension of RxArrays with connect capabilities.

import { RxConnectArrays, RxMap. RxSet } from '@ns3/rx-state';

interface Product {
  id: string;
  price: number;
}

const fetching = new RxSet<string>();
const products = new RxMap<string, Product>();
const paginated = new RxConnectArrays({
  itemsMap: products,
  itemKey: (value) => value.id,
  connectingSet: fetching, // optional
  timeout: 5 * 60 * 1000, // 5 minutes
  scope: 'all',
  strategy: 'eager',
});

paginated.connect$(
  'page-1',
  () => ajax('http://link-to-my-endpoint/page-1'
)).subscribe(console.log);

products.connect$ - will return state data for given key and use provided function to retrieve data if none is present or is outdated.

fetching - will keep information if connect$ method is at the moment using provided function to retrieve data for given key.

To learn more about connect$ capabilities read Connect.

RxArray and RxConnectArray

These data structures are the same as RxArrays and RxConnectArrays respectively, but do not support multiple keys.

Methods like get$, set, connect$ are missing the first argument (key).

Connect

Connect Options

For connect options we distinguish multiple and single connections.

  • We use multiple for data structures keeping more than one entity using keys.
  • We use single for data structures keeping only one value.

The base connect$ options consist of:

interface Options {
  timeout?: number;
  strategy?: 'eager' | 'lazy';
  scope?: 'single' | 'all'; // only in multiple connections
}
  • timeout - For how long (in milliseconds) data should be considered valid. After this time calling connect$ method will cause ConnectionManager to use provided function to retrieve data. Before that it serves data from cache (state).
    • Defaults to never, meaning that data will always remain valid. Note that technically I am setting timeout to "week" (604800000), which should be more than enough. If, for some reason, you need more than week you need to set it manually.
    • Setting 0 will make data always invalid.
  • strategy - Invalidation strategy, meaning how eagerly ConnectionManager invalidates data.
    • eager - Calling connect$ method on invalid data will cause ConnectionManager to NOT return any data and wait for the provided function to retrieve data.
    • lazy - Calling connect$ method on invalid data will cause ConnectionManager to return outdated data immediately and use provided function to retrieve data in the background.
    • defaults to eager.
  • scope - Describes if invalidation should affect only specific target or entire collection. This only applies to multiple connections.
    • single - Only element with a given key is considered invalid. This makes sense when elements are not correlated, like individual items etc.
    • all - If at least one element is invalid we invalidate all of them. This makes sense when elements are correlated like pages in pagination.
    • defaults to single.

It is common for connect-able structures to also accept one additional optional input.

For single connection it is connecting$ (BehaviorSubject) that keeps info if function provided to retrieve data is currently executing.

For multiple connections it is connectingSet (RxSet) that does the same but for multiple connections.

connect$ method

connect$ method will accept:

  • key and function to retrieve data for multiple connections.
  • function to retrieve data for single connection.

The function to retrieve data that we provide will be called if there is no data or data is outdated.

It is NOT limited to http calls. It can be http call, socket connection, or reading from file system. To only requirement is that it is a function that returns Observable with given type of data.

Example

This is only a simple example. This is not the only nor the best way to use those reactive data structures. This is meant to give you an easy place to get started.

Create one of your stores:

export class ProductsStore {
  public readonly deleting = new RxSet<string>();
  public readonly updating = new RxSet<string>();
  public readonly fetching = new RxSet<string>();
  public readonly entities = new RxConnectMap<string, Product>({
    connectingSet: this.fetching,
    timeout: 5 * 60 * 1000, // 5 minutes
    scope: 'single',
    strategy: 'lazy',
  });

  // service for making http calls
  constructor(private readonly service: ProductsService) {}

  connect$(id: string): Observable<Product> {
    return this.entities.connect$(id, () => this.service.get(id));
  }

  patch(id: string, value: Partial<Product>): Observable<Product> {
    this.updating.add(id);

    return this.service.patch(id, value).pipe(
      tap({
        next: (product) => {
          this.updating.delete(id);
          this.entities.set(product.id, product);
        },
        error: () => {
          this.updating.delete(id);
        },
      }),
    );
  }

  delete(id: string): Observable<void> {
    this.deleting.add(id);

    return this.service.delete(id).pipe(
      tap({
        next: () => {
          this.entities.delete(id);
          this.deleting.delete(id);
        },
        error: () => {
          this.deleting.delete(id);
        },
      }),
    );
  }
}

Usage in React

Export your ProductsStore in a way that is most comfortable.

You can create instance and export it, or use dependency injection system to abstract creation process. It is your choice.

To use RxJs in React I recommend using useStream and useStreamValue hooks from @ns3/react-utils.

In your component:

import { useStream } from '@ns3/react-utils';
import { useProductQuery, ProductCont, ErrorComp, LoaderComp } from './somewhere';

export default function ProductDetails() {
  const { id } = useProductQuery();
  const result = useStream(() => productsStore.connect$(id), [id]);

  if (result.status === 'error') {
    return <ErrorComp error={result.error} />;
  }

  if (result.status === 'pending') {
    return <LoaderComp />;
  }

  return <ProductCont product={result.value} />;
}

That is it. When useProductQuery emits new id connect$ method will be called again and data will change. If you alter product in anyway it will be reflected here as well.

Word about useStream and useStreamValue

  • useStream is meant to be used with pending streams. Ones that not necessarily return data right away.

    • connect$ may take some time to retrieve data. This will result in result.status === 'pending'.
    • connect$ may also fail. This will result in result.status === 'error'.
  • useStreamValue is meant to be used with observables that always return data and do not throw. Fortunately, excluding connect$ method, all structures that we discussed here will always immediately emit some data. Note that if, for example, RxMap does not have real value for a given key it will emit undefined, but emit it will! If you use useStreamValue for pending or erroring streams it will return just undefined in that case of pending or error.