@capitec/omni-state
v0.1.3
Published
Simple web app state and storage management
Downloads
259
Readme
Introduction
Omni State is a collection of utilities that makes it simple to manage the local state and data storage in web applications. The library is lightweight and comes with zero runtime dependencies, minimizing bloat to your project.
Core features of the library include:
- ObservableProperty - Provides an observable property that enables immutable editing of state using a draft before persisting model.
- StatefulProperty - An extension of the ObservableProperty that persists the state data to a provided store when set, e.g. local storage, session storage, or a custom store.
- Storage decorator - A decorator that allows you to annotate any class property to persist its value to storage when set.
- Local & Session Data Stores - Default implementation are provided to persist data to the browse local storage and session storage.
- Custom Data Stores - Custom data stores can be created by implementing either the SyncStorage or AsyncStorage interfaces, enabling you to e.g. persist data online when a property is set.
Usage
1️⃣ Install the library in your project.
npm install @capitec/omni-state
2️⃣ Use any of the example property patterns below where you implement state management in your app. Here we provide an example using global singleton to manage the state of an app.
// e.g. my-app/AppState.ts
import { LocalStorage, ObservableProperty, SessionStorage, StatefulProperty, stateExperimental } from '@capitec/omni-state';
import { MyCustomStore } from './stores/MyCustomStore';
export type Person = {
firstName: string;
lastName: string;
}
export class AppState {
// OBSERVABLE PROPERTY
// A simple in-memory property that can be observed for changes, e.g.:
simpleObservable = new ObservableProperty<Person>();
// STATEFUL PROPERTY
// An observable property that is persisted to SessionStorage when set, e.g.:
statefulSessionObservable = new StatefulProperty({ storage: SessionStorage, key: 'statefulSessionObservable' });
// An observable property that is persisted to LocalStorage when set, e.g.:
statefulLocalObservable = new StatefulProperty({ storage: LocalStorage, key: 'statefulLocalObservable' });
// An observable property that is persisted to a custom async store when set, e.g.:
statefulCustomObservable = new StatefulProperty({ storage: MyCustomStore, key: 'statefulCustomObservable' });
// STATE DECORATOR - using Typescript "experimentalDecorators": true
// A simple property that is persisted to SessionStorage when set, e.g.:
@stateExperimental({ storage: SessionStorage, key: 'decoratorSession' })
decoratorSession?: string;
// A simple property that is persisted to LocalStorage when set, e.g.:
@stateExperimental({ storage: LocalStorage, key: 'decoratorSession' })
decoratorLocal?: string;
// A simple property that is persisted to a custom async store when set, e.g.:
@stateExperimental({ storage: MyCustomStore, key: 'decoratorCustom' })
decoratorCustom?: string;
// OBSERVABLE PROPERTY + STATE DECORATOR = STATEFUL PROPERTY
// A simple property that is persisted to SessionStorage when set, e.g.:
@stateExperimental({ storage: SessionStorage, key: 'observableSession' })
observableSession = new ObservableProperty<string>();
// A simple property that is persisted to LocalStorage when set, e.g.:
@stateExperimental({ storage: LocalStorage, key: 'observableLocal' })
observableLocal = new ObservableProperty<string>();
// A simple property that is persisted to a custom async store when set, e.g.:
@stateExperimental({ storage: MyCustomStore, key: 'observableCustom' })
observableCustom = new ObservableProperty<string>();
/**
* Get an instance of the shared state.
*
* @returns The shared state instance.
*/
static getInstance() {
if (!AppState.instance) {
AppState.instance = new SharedState();
}
return AppState.instance;
}
/**
* Initialize App global state, read persisted settings.
*
* Note: Only to be called once by the app entrypoint.
*
* @returns Nothing.
*/
async init() {
// Ensure all state properties have been initialized from storage.
await StateManager.allSettled;
}
}
3️⃣ Make use of any of the below patterns to access and mutate the app state properties. Note both ObservableProperty and StatefulProperty implement the observable pattern, allowing you to .get(), .set(), and .subscribe() to the property. Decorated properties are implemented as standard properties, thus they can be get and set like any other primitive or object.
// my-app/App.ts
import { AppState } from './AppState';
class App {
private appState: AppState;
constructor() {
this.appState = AppState.getInstance();
}
async init(): void {
// Ensure all app state values are loaded from storage.
await appState.init();
// USING OBSERVABLE OR STATEFUL PROPERTIES
// Initializing an observable (or stateful) property.
appState.simpleObservable.set({
firstName: 'Hello',
lastName: 'World';
});
// Subscribing to value changes on an observable (or stateful) property.
appState.simpleObservable.subscribe(value => {
console.log(value);
});
// Editing specific values on an observable (or stateful) property.
appState.simpleObservable.set(draft => {
draft.firstName = 'Test';
});
// USING STATE DECORATOR PROPERTIES
// Setting a decorated property.
this.decoratorLocal = 'Test';
// Getting a decorated property
console.log(this.decoratorLocal);
}
}
await new App().init();
4️⃣ Omni State exposes implementations for LocalStorage and SessionStorage stores. However, you can implement a custom store by creating an implementation of either the SyncStorage or AsyncStorage interfaces.
The SyncStorage interface is used to implement the LocalStorage and SessionStorage stores, while the AsyncStorage interface allows you to build a custom storage implementation that can persist data to environments that have to be contacted asynchronously, e.g. saving values to an online service.
// my-app/stores/MyCustomStore.ts
import { SyncStorage } from '@capitec/omni-state';
/**
* Simple wrapper around the browser `localStorage` that simplifies storing values across browser sessions.
*
* Values are persisted to storage as JSON strings, and can be read back as typed objects.
*/
class MyCustomStoreImpl implements SyncStorage {
/**
* Gets a value from storage for the given key.
*
* @param key - The key under which the value is stored.
*
* @returns The stored value parsed from JSON, or null if not set.
*/
get<T>(key: string): T | undefined {
try {
const result = window.localStorage.getItem(key);
console.log(`Reading Key: ${key}`, result);
if (!result) {
return undefined;
}
return JSON.parse(result) as T;
} catch (err) {
return undefined;
}
}
/**
* Sets a value in storage for the given key.
*
* @param key - The key under which to store the value.
* @param value - The value to store.
*
* @returns Nothing.
*/
set(key: string, value: unknown): void {
console.log(`Writing Key: ${key}`, value);
window.localStorage.setItem(key, JSON.stringify(value));
}
/**
* Removes a value from storage for the given key.
*
* @param key - The key of the value to remove.
*
* @returns Nothing.
*/
remove(key: string): void {
console.log(`Removing Key: ${key}`);
window.localStorage.removeItem(key);
}
/**
* Removes all values from storage.
*
* @returns Nothing.
*/
clear(): void {
console.log(`Clearing all keys`);
window.localStorage.clear();
}
/**
* Get the name of the key at a given index.
*
* @param index - The index number to get the key name for.
*
* @returns The name of the key at the index.
*/
key(index: number): string | undefined {
return window.localStorage.key(index) || undefined;
}
/**
* Finds a list of all keys in storage.
*
* @returns The list of keys in storage.
*/
keys(): string[] {
return Object.keys(window.localStorage);
}
/**
* Get the number of items in storage.
*
* @returns The storage item count.
*/
size(): number {
return window.localStorage.length;
}
}
export const MyCustomStore = new MyCustomStoreImpl();
5️⃣ To make use of the experimental decorators, ensure your environment configurations includes the following:
TypeScript
If using TypeScript, include this in your tsconfig.json
:
{
"experimentalDecorators": true
}
Babel
If using Babel, include this in your .babelrc
:
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"version": "2018-09",
"decoratorsBeforeExport": true
}
]
]
}
Webpack
If using Webpack, include this in your webpack.build.js
:
export default {
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
exclude: /node_modules[\\/](?!(omni-components|omni-router|omni-state)[\\/]).*/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
['@babel/transform-runtime'],
[
'@babel/plugin-proposal-decorators', {
version: '2018-09',
decoratorsBeforeExport: true
}
]
]
}
}
]
}
]
}
};
Contributors
Made possible by these fantastic people. 💖
See the CONTRIBUTING.md
guide to get involved.
License
Licensed under MIT