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

@norabytes/reflexive-store

v2.2.2

Published

A powerful state management library built-on-top of the RxJS library.

Downloads

391

Readme

NoraBytes © 2024

ReflexiveStore

Installation

npm install @norabytes/reflexive-store

or

yarn add @norabytes/reflexive-store

Breaking Changes

  • v2.2.0:
    • The ReflexiveStore is not an abstract class anymore, this means that now you can directly do const store = new ReflexiveStore().
    • The protected onStoreInit, onDispose and extractStoreContextByDotNotation methods have been refactored/removed.
      • onStoreInit: Has been refactored to be a global callback register.
      • onDispose: Has been replaced by the onStoreDispose global callback register.
      • extractStoreContextByDotNotation: Has been removed.
    • Renamed the disposeEvent$ property to storeDisposeEvent$

Usage

How it works

The ReflexiveStore is built on top of the RxJS library, this means that you can create very complexe responsive flows by leveraging the RxJS pipe and its operators.

Before diving into how to use the ReflexiveStore, it is important to understand its core features.

StoreContext

The core of the ReflexiveStore is the StoreContext. It is the object which you'll use the most because each property declared in the StoreModel it'll be wrapped into a StoreContext which exposes the methods and properties you can use to interact with the property.

There are 3 methods and 2 properties.

  • [PROPERTY] subject - Is the low-level RxJS BehaviorSubject, this means that all the values from your store are actually BehaviorSubject.
  • [PROPERTY] value$ - Is the low-level RxJS Observable, this means that you can subscribe to any value from your store and be notified when the value has changed.
  • [METHOD] setValue - Can be used to update in real-time the value of any property from the store.
  • [METHOD] onChange - Can be used to register a callback method which will be invoked whenever the value changes.
  • [METHOD] getValue - Can be used to imperatively retrieve the value of any property from the store. (This is not the best approach in the reactive world of RxJS)

Now that we know that all of our store properties are just a bunch of StoreContext objects, we can move forward with some examples.

StoreModel

The ReflexiveStore heavily relies on TypeScript and its dynamic (generic) types, therefore, in order to correctly display the StoreContext methods/properties in the intellisense, the store model will be wrapped into a generic mapper type named StoreMap.

You'll not have to use it directly, but it is helpful to know about its existance in order to better understand the big picture.

Start by creating a StoreModel interface:

Avoid marking the properties of your StoreModel as Optional with ? as this could interfere with the generation of the StoreMap generic type. Instead use <type> | undefined.

// ./contact-us-form/store.model.ts

import type { ReflexiveDetachedValue } from '@norabytes/reflexive-store';

export interface ContactUsFormStoreModel {
  firstName: string;
  middleName: string | undefined;
  lastName: string;
  dob: ReflexiveDetachedValue<{
    day: number;
    month: number;
    year: number;
  }>;
  info: {
    primary: string;
    secondary: string;
    additional: ReflexiveDetachedValue<Record<string, string>>;
    extra: {
      marriage: {
        isMarried: boolean;
        spouseFullName: string;
      };
      children: {
        count: number;
        list: Child[];
      };
    };
  };
  storedFunction: ReflexiveDetachedValue<() => void>;
}

DetachedValue

The DetachedValue is just a simple wrapper which we use to inform the StoreMap generic mapper type to not recursively wrap a property children with the StoreContext type.

To better understand the purpose, let's throw in some TS code.

// Let's say that we want to get the value of the `firstName` property, should be easy enough by doing:

const firstName = store.firstName.getValue();

We easily retrieved the current value of the firstName property by just using the imperative getValue method from the StoreContext.

// Now, we want to change the `lastName` property:

let previousLastName: string;

store.lastName.setValue((currentLastName) => {
  previousLastName = currentLastName;

  return 'Auditore';
});

console.log(previousLastName);
// => Whatever value it had before we changed it

console.log(store.lastName.getValue());
// => 'Auditore'

So far nothing special, now let's decide that we want to update the dob property, we know that we can just do store.dob.day.setValue(), store.dob.month.setValue() and store.dob.month.setValue()...

It starts to not feel right, can't we just update the entire dob object with a single invokation of the StoreContext.setValue method?

Yes we can! And that's exactly why we used the DetachValue type.

// Visual representation of the `ContactUsFormStoreModel` type.

{
  firstName: StoreContext<string>;
  middleName: StoreContext<string | undefined>;
  lastName: StoreContext<string>;
  dob: StoreContext<{ day: number; month: number; year: number }>; // Pay attention here, to the `info`, `extra`, `marriage`, `children` & `storedFunction` property type.
  info: StoreMap<{
    primary: StoreContext<string>;
    secondary: StoreContext<string>;
    additional: StoreContext<Record<string, string>>;
    extra: StoreMap<{
      marriage: StoreMap<{
        isMarried: StoreContext<boolean>;
        spouseFullName: StoreContext<string>;
      }>;
      children: StoreMap<{
        count: StoreContext<number>;
        list: StoreContext<Child[]>;
      }>;
    }>;
  }>;
  storedFunction: StoreContext<() => void>;
}

As you can see, the dob property is not wrapped within a StoreMap and its properties, day, month and year are not wrapped within a StoreContext.

// This means that if we try to do this:

store.dob.day.setValue(22);

// We'll get an error like "The `day` property does not exist on the `dob` property."

// So, to correctly update the `dob` property, we must do:

store.dob.setValue({
  day: 29,
  month: 09,
  year: 1969,
});

console.log(store.dob.getValue());
// => `{ day: 29, month: 09, year: 1969, }`

Basically, you should use the DetachedValue type in your StoreModel in the following scenarios:

  • When you want to store a function or a class into a property.
  • When you don't want to have all the children of a property to be mutated to a StoreContext object.
  • When you have a very complex object which would become too brittle to manage it by having all its properties mutated to a StoreContext object. (Imagine saving an HTTP Request object into the store without using the DetachValue, it'll be pure chaos)

The list above isn't exhaustive and of course it highly depends on the application/logic we are working on.

Simple Implementation

// store.model.ts

import type { ReflexiveDetachedValue } from '@norabytes/reflexive-store';

export interface StoreModel {
  counter: number;
  userData: ReflexiveDetachedValue<{
    firstName: string;
    lastName: string;
  }>;
}

// store.ts

import { ReflexiveStore } from '@norabytes/reflexive-store';

export class Store extends ReflexiveStore<StoreModel> {
  // However you can skip overriding the internal `onStoreInit` method.
  protected override onStoreInit(): void {
    console.log(`The 'AppStore' has been successfully initialized.`);
  }

  // However you can skip overriding the internal `onDispose` method.
  protected override onDispose(): void {
    console.log(`The 'AppStore' is being disposed.`);
  }
}

// app.ts

import { ReflexiveStore, ReflexiveDetachedValue } from '@norabytes/reflexive-store';
import { debounceTime } from 'rxjs';
import { Store } from './store';

class App {
  appStore: Store;

  constructor() {
    this.store = new Store();
  }

  onInit(): void {
    this.store.initStore({
      count: 0,
      userData: new ReflexiveDetachedValue({
          firstName: '',
          lastName: '',
        })
    });

    this.subscribeToCounterChanges();
    this.subscribeToUserDataChanges();
  }

  onAppDispose(): void {
    this.appStore.dispose();
  }

  incrementCounter(): void {
    this.appStore.store.count.setValue((p) => p + 1);
  }

  updateUserData(firstName: string, lastName: string): void {
    this.appStore.store.userData.setValue({
      firstName,
      lastName,
    });
  }

  private subscribeToCounterChanges(): void {
    this.appStore.store.counter.onChange((counterValue) => {
      console.log('Counter is now at', counterValue);
    });
  }

  private subscribeToUserDataChanges(): void {
    this.appStore.store.userData.onChange({
      with: [debounceTime(250)],
      do: (newUserData) => {
        this.fictionalApiService.updateUserData(newUserData);
      }
    });
  }
}

// ./button

onClick={app.incrementCounter()}

Live Examples

You can see and test in real-time some examples by accessing this CodeSandbox link.

ReactJS Plugin

The ReflexiveStore has a native plugin which can be used with ReactJS, check it out at https://www.npmjs.com/package/@norabytes/reactjs-reflexive-store.