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.
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:
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$()
(or map.get()
if using BehaviorMap
)undefined
. This is different than the standard JS Map class which
could return undefined
if the value was not set. The reason for this because callers need something to subsribe to. 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 ReplaySubjectsnew 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 underlying Map
has(key: K): boolean
- returns if a given key existsset(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 return falsy
).emitError(key: K, error: any): this
- calls error()
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 call delete(key)
to remove the key/value from the underlying Map
.clear(): void
- will clear out the underlying Map
of all key/value pairs. It will call complete()
on all observablesdelete(key: K): boolean
- returns false
if the key did not exist. Returns true
is the key did exists. It then calls complete()
on the observable and removes that key/value pair from the underlying Map
keys(): IterableIterator<K>
- returns an iterable iterator for the underlying Map
's keysforEach$(callbackfn: (value: Observable<V>, key: K) => void): void
- takes a callback function that will be applied to all the underlying Map
's observablesentries$ (): IterableIterator<[K, Observable<V>]>
- returns an iterable iterator over key/value pairs of the underlying Map
where the value is the key's observablevalues$ (): IterableIterator<Observable<V>>
- returns an iterable iterator over the underlying Map
's values as observablesBehaviorMap
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 the initialValue
passed into the constructor.forEach (callbackfn: (value: V, key: K) => void): void
- takes a callback function that will be applied to all the underlying Map
's observables valuesvalues (): IterableIterator<V>
- returns an iterable iterator over the underlying Map
's current observable values entries (): IterableIterator<[K, V]>
- returns an iterable iterator over key/value pairs of the underlying Map
where the value is the key's current observable valueThis is a simple RxJS implementation of Redux and state management.
- 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:
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 classpublic 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 statepublic getState (): WithPreviousState<T>
- returns the current state synchronouslypublic destroy (): void
- calls complete()
on the underlying BehaviorSubject. Once a store has destroyed, it can no longer be usedprotected dispatch (state: Partial<T>): void
- updates the state with the passed in state then calls next()
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
# 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
Generated using TypeDoc