Simple RxJS implementations for common classes used across many different types of projects. Implementations include: redux-like store (for dynamic state management), rxjs maps, rxjs lists (useful for caching), and rxjs event emitters.
- Installation - Install the package and basic imports
- Available Features
- Observable Maps - Basic JavaScript Map wrapper to allow use of observables.
- Base Store - Simple Redux implementation built on RxJS that is flexible and dynamic
- Future Features - Features coming soon
- Contributing
Install rxjs and rxjs-util-classes (rxjs v6.x is required):
npm install --save rxjs rxjs-util-classes
# or via yarn
yarn add rxjs rxjs-util-classes
Importing:
import { ObservableMap } from 'rxjs-util-classes';
// or
const { ObservableMap } = require('rxjs-util-classes');
This is a wrapper around the native JavaScript Map except it returns observables. There are three main map types:
- ObservableMap - uses RxJS Subject
- BehaviorMap - uses RxJS BehaviorSubject
-
ReplayMap - uses RxJS ReplaySubject
- Available Map API methods - API methods for each map class
See the Maps API and Important Notes about ObservableMaps for additional information
See map recipes for commom use cases.
Uses the standard RxJS Subject so subscribers will only receive values emitted after they subscribe. (Full API)
import { ObservableMap } from 'rxjs-util-classes';
const observableMap = new ObservableMap<string, string>();
observableMap.set('my-key', 'this value will not be received');
observableMap.get$('my-key').subscribe(
value => console.log('Value: ' + value),
error => console.log('Error: ' + error),
() => console.log('complete')
);
// `.set()` will emit the value to all subscribers
observableMap.set('my-key', 'first-data');
observableMap.set('my-key', 'second-data');
// delete calls `.complete()` to clean up
// the observable
observableMap.delete('my-key');
// OUTPUT:
// Value: first-data
// Value: second-data
// complete
Uses the RxJS BehaviorSubject so subscribers will always receive the last emitted value. This class requires an initial value to construct all underlying BehaviorSubjects. (Full API)
import { BehaviorMap } from 'rxjs-util-classes';
const behaviorMap = new BehaviorMap<string, string>('initial-data');
behaviorMap.get$('my-key').subscribe(
value => console.log('Value: ' + value),
error => console.log('Error: ' + error),
() => console.log('complete')
);
behaviorMap.set('my-key', 'first-data');
behaviorMap.set('my-key', 'second-data');
// emitError calls `.error()` which ends the observable stream
// it will also remove the mey-value from the map
behaviorMap.emitError('my-key', 'there was an error!');
// OUTPUT:
// Value: initial-data
// Value: first-data
// Value: second-data
// Error: there was an error!
Uses the RxJS ReplaySubject so subscribers will receive the last nth
emitted values. This class requires an initial replay number to construct all underlying ReplaySubject. (Full API)
import { ReplayMap } from 'rxjs-util-classes';
const replayMap = new ReplayMap<string, string>(2);
replayMap.set('my-key', 'first-data');
replayMap.set('my-key', 'second-data');
replayMap.set('my-key', 'third-data');
replayMap.set('my-key', 'fourth-data');
replayMap.get$('my-key').subscribe(
value => console.log('Value: ' + value),
error => console.log('Error: ' + error),
() => console.log('complete')
);
// delete calls `.complete()` to clean up
// the observable
replayMap.delete('my-key');
// OUTPUT:
// Value: third-data
// Value: fourth-data
// complete
-
map.get$()
(ormap.get()
if usingBehaviorMap
)- This will always return an Observable and never return
undefined
. This is different than the standard JS Map class which could returnundefined
if the value was not set. The reason for this because callers need something to subsribe to.
- This will always return an Observable and never return
The ObservableMap, BehaviorMap, & ReplayMap all share the same methods. The only exception is the constructors
and BehaviorMap
has some additional synchornous methods. Any method that returns an observable will following the standard practice of ending with a $
. All methods listed are public
See the Maps API for more details
K
= generic type defined as map keys (string | number | boolean
)
V
= generic type defined as map value (any
)
-
new ObservableMap<K, V>()
- blank constructor. All underlying observables will be constructed with the standard Subject. -
new ReplayMap<K, V>(relayCount: number)
- number of replays to share. Number passed in will be passed to all underlying ReplaySubjects -
new BehaviorMap<K, V>(initialValue: V)
- initial value to start each observable with. Value will be passed into each underlying BehaviorSubject
-
size
- value of the current size of the underlyingMap
-
has(key: K): boolean
- returns if a given key exists -
set(key: K, value: V): this
- emits a value on the given key's observable. It will create an observable for the key if there is not already one. -
get$ (key: K): Observable<V>
- returns the observable for a given key. It will create an observable for the key if there is not already one _(meaning this will never returnfalsy
). -
emitError(key: K, error: any): this
- callserror()
on the given key's underlying subject. Emiting an error on an observable will terminate that observable. It will create an observable for the key if there is not already one. It will then calldelete(key)
to remove the key/value from the underlyingMap
. -
clear(): void
- will clear out the underlyingMap
of all key/value pairs. It will callcomplete()
on all observables -
delete(key: K): boolean
- returnsfalse
if the key did not exist. Returnstrue
is the key did exists. It then callscomplete()
on the observable and removes that key/value pair from the underlyingMap
-
keys(): IterableIterator<K>
- returns an iterable iterator for the underlyingMap
's keys -
forEach$(callbackfn: (value: Observable<V>, key: K) => void): void
- takes a callback function that will be applied to all the underlyingMap
's observables -
entries$ (): IterableIterator<[K, Observable<V>]>
- returns an iterable iterator over key/value pairs of the underlyingMap
where the value is the key's observable -
values$ (): IterableIterator<Observable<V>>
- returns an iterable iterator over the underlyingMap
's values as observables
Because Behavior Subjects keep their last value, we can interact with that value synchronously.
-
get(key: K): V
- returns the current value of a given key. It will create an observable for the key if there is not already one which will return theinitialValue
passed into the constructor. -
forEach (callbackfn: (value: V, key: K) => void): void
- takes a callback function that will be applied to all the underlyingMap
's observables values -
values (): IterableIterator<V>
- returns an iterable iterator over the underlyingMap
's current observable values -
entries (): IterableIterator<[K, V]>
- returns an iterable iterator over key/value pairs of the underlyingMap
where the value is the key's current observable value
This is a simple RxJS implementation of Redux and state management.
- BaseStore - uses RxJS BehaviorSubject to distribute and manage application state
- See the Base Store API for the full API
- See store recipes for commom use cases.
Redux is a very popular state management solution. The main concepts of redux-like state is that:
- State in a central location (in the "store")
- State can only be modified by "dispatching" events to the "store" that are allowed/configured
This makes keeping track of state uniformed because the state is always in one location, and can only be changed in ways the store allows.
One huge advantage of this implementation is its ability to have a dyanmic store. See the Dynamic Store recipe for further details and implementation.
src/app-store.ts (pseudo file to show store implementation)
import { BaseStore } from 'rxjs-util-classes';
export interface IUser {
username: string;
authToken: string;
}
export interface IAppState {
isLoading: boolean;
authenticatedUser?: IUser;
// any other state your app needs
}
const initialState: IAppState = {
isLoading: false,
authenticatedUser: undefined
};
/**
* Extend the BaseStore and expose methods for components/services
* to call to update the state
*/
class AppStore extends BaseStore<IAppState> {
constructor () {
super(initialState); // set the store's initial state
}
public setIsLoading (isLoading: boolean): void {
this.dispatch({ isLoading });
}
public setAuthenticatedUser (authenticatedUser?: IUser): void {
this.dispatch({ authenticatedUser });
}
// these methods are inherited from BaseStore
// getState(): IAppState
// getState$(): Observable<IAppState>
}
/* export a singleton instance of the store */
export const store = new AppStore();
src/example-component.ts (pseudo component that will authenticate the user and interact with the app's state)
import { store, IUser, IAppState } from './app-store';
/**
* Function to mock an authentication task
*/
function authenticate () {
return new Promise(res => {
setTimeout(() => {
res({ username: 'bob-samuel', authToken: 'qwerty-123' });
}, 1000);
});
}
store.getState$().subscribe((appState: IAppState) => {
/* do something with the state as it changes;
maybe show a spinner or the authenticatedUser's username */
});
/* authenticate to get the user */
store.setIsLoading(true);
authenticate()
.then((user: IUser) => {
/* here we set the store's state via the methods the
store exposed */
store.setAuthenticatedUser(user);
store.setIsLoading(false);
})
.catch(err => store.setIsLoading(false));
BaseStore
is an abstract class and must be extended.
T
= generic type defined as the state type ({ [key: string]: any }
)
WithPreviousState<T>
= generic type defined as the state type (T & { __previousState: T }
where T = { [key: string]: any }
)
-
protected constructor (initialState: T)
- construct with the intial state of the store. Must be called from an extending class -
public getState$ (): Observable<WithPreviousState<T>>
- returns an observable of the store's state. Underlying implementation uses a BehaviorSubject so this call will always receive the current state -
public getState (): WithPreviousState<T>
- returns the current state synchronously -
public destroy (): void
- callscomplete()
on the underlying BehaviorSubject. Once a store has destroyed, it can no longer be used -
protected dispatch (state: Partial<T>): void
- updates the state with the passed in state then callsnext()
on the underlying BehaviorSubject. This will do a shallow copy of the state using the spread operator (...
). This is to keep state immutable.
You can access the immediate
- WildEmitter implementation
- List Map for caching and watching changes on a list
# clone repo
git clone https://github.com/djhouseknecht/rxjs-util-classes.git
# move into directory
cd ./rxjs-util-classes
# install
npm install
All commits must be compliant to commitizen's standard. Useful commit message tips can be found on angular.js' DEVELOPER.md.
# utility to format commit messages
npm run commit
If you are releasing a new version, make sure to update the CHANGELOG.md The changelog adheres to Keep a Changelog.
Know what messages will trigger a release. Check semantic-release defaults and any added to
./package.json#release
Deploying is managed by semantic-release. Messages must comply with commitizen see above.
Testing coverage must remain at 100% and all code must pass the linter.
# lint
npm run lint
# npm run lint:fix # will fix some issues
npm run test
npm run build
- Add more features