@norabytes/reflexive-store
v2.2.2
Published
A powerful state management library built-on-top of the RxJS library.
Downloads
391
Maintainers
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 anabstract
class anymore, this means that now you can directly doconst store = new ReflexiveStore()
. - The
protected
onStoreInit
,onDispose
andextractStoreContextByDotNotation
methods have been refactored/removed.onStoreInit
: Has been refactored to be a globalcallback
register.onDispose
: Has been replaced by theonStoreDispose
globalcallback
register.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-levelRxJS
BehaviorSubject, this means that all the values from your store are actuallyBehaviorSubject
. - [PROPERTY]
value$
- Is the low-levelRxJS
Observable, this means that you cansubscribe
to any value from your store and benotified
when the value has changed. - [METHOD]
setValue
- Can be used toupdate
in real-time thevalue
of any property from the store. - [METHOD]
onChange
- Can be used toregister
acallback
method which will be invoked whenever thevalue
changes. - [METHOD]
getValue
- Can be used toimperatively
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
asOptional
with?
as this could interfere with the generation of theStoreMap
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 aclass
into a property. - When you don't want to have all the
children
of aproperty
to be mutated to aStoreContext
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 anHTTP Request
object 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.