@norabytes/reflexive-store v2.2.2
NoraBytes © 2024
ReflexiveStore
Installation
npm install @norabytes/reflexive-store
or
yarn add @norabytes/reflexive-store
Breaking Changes
v2.2.0:- The
ReflexiveStoreis not anabstractclass anymore, this means that now you can directly doconst store = new ReflexiveStore(). - The
protectedonStoreInit,onDisposeandextractStoreContextByDotNotationmethods have been refactored/removed.onStoreInit: Has been refactored to be a globalcallbackregister.onDispose: Has been replaced by theonStoreDisposeglobalcallbackregister.extractStoreContextByDotNotation: Has been removed.
- Renamed the
disposeEvent$property tostoreDisposeEvent$
- The
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-levelRxJSBehaviorSubject, this means that all the values from your store are actuallyBehaviorSubject. - PROPERTY
value$- Is the low-levelRxJSObservable, this means that you cansubscribeto any value from your store and benotifiedwhen the value has changed. - METHOD
setValue- Can be used toupdatein real-time thevalueof any property from the store. - METHOD
onChange- Can be used toregisteracallbackmethod which will be invoked whenever thevaluechanges. - METHOD
getValue- Can be used toimperativelyretrieve 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
StoreContextobjects, 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
StoreModelasOptionalwith?as this could interfere with the generation of theStoreMapgeneric 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
functionor aclassinto a property. - When you don't want to have all the
childrenof apropertyto be mutated to aStoreContextobject. - When you have a very complex object which would become too brittle to manage it by having all its properties mutated to a
StoreContextobject. (Imagine saving anHTTP Requestobject into the store without using theDetachValue, 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.