Vex is a simple, lightweight, asynchronous state manager for JavaScript user interfaces.
npm install @dannymayer/vex
state$: Observable<StateType>
An Observable of the manager's state. Will emit at least one value to any subscriber.
dispatch(action: Action): void
Dispatches an Action.
once(action: Action): Observable<ActionResult<StateType>>
Dispatches an Action and returns an Observable of the result.
dispatches(actionType?: string): Observable<ActionResult<StateType>>
Returns an Observable that emits each time an action is dispatched, and before it resolves. If anactionType
is provided, filters the returned Observable to only emit dispatches of thatactionType
.
results(actionType?: string): Observable<ActionResult<StateType>>
Returns an Observable that emits each time an action is resolved. If anactionType
is provided, filters the returned Observable to only emit results of thatactionType
.
parameter
initialState: StateType
(required) Each manager must be initialized with aninitialState
.
parameter
options?: ManagerOptions
(optional)
allowConcurrency: boolean
(optional) Defaults totrue
; if set tofalse
, an Action dispatched before the previous Action has resolved will be queued and executed immediately when the previous Action resolves (using RxJS'sconcatMap
).
type: string
(required) A string representing the category of the action.
reduce(state: StateType): StateType
(required) The business logic associated with a synchronous Action. Analagous to a reducer function in Redux, in that it returns the new state of the manager.
resolve(state$: Observable<StateType>): Promise<StateType> | Observable<StateType>
(required) The business logic associated with an asynchronous Action. Returns a Promise or Observable of the new state of the manager.
state: StateType
(required) A snapshot of the state at the time theActionResult
was created.
actionType: string
(required)
error?: any
(optional)
Vex integrates with Redux DevTools to allow you to visualize your app's state over time, including the ability to time-travel through your app's history.
To configure DevTools, simply call setUpDevTools
with an optional DevToolsOptions
as the only argument.
In Angular, setUpDevTools
must be invoked inside of an NgZone#run
callback, like so:
import { Component, NgZone } from '@angular/core'
import { setUpDevTools } from '@dannymayer/vex'
@Component(/* ... */)
export class AppComponent {
constructor(private _ngZone: NgZone) {
this._ngZone.run(() => setUpDevTools())
}
}
(all fields are optional)
name: string
maxAge: number
latency: number
actionsBlacklist: string[]
actionsWhitelist: string[]
shouldCatchErrors: boolean
logTrace: boolean
predicate: (state: any, action: any) => boolean
shallow: boolean
Why Vex? The short answer: it's async by default!
The functional-reactive style enabled by RxJS has changed the way we approach asynchrony in our code, and many state management frameworks have been built that use Observables to model application state changing over time. Functional-reactive programming is also great for doing asynchronous things like HTTP requests, but I haven't seen a state management framework that embraces this at its core; support for "side-effects" always feels like an add-on.
Well, I'm vexed.
I wanted my state manager to be simple and practical, and not too prescriptive; I want my state management to feel like part of my app's architecture, rather than like something I have to build my app around. I was frustrated with the amount of boilerplate in Flux-style state management and with the fact that asynchrony felt like a second-class citizen. I knew I wanted to keep the functional-reactive style of NgRx along with the event-sourcing feel it inherits from Flux, and I loved the ergonomic, low-boilerplate implementation that Akita offers. Vex aims to check all of those boxes in one tiny, convenient interface.
app.model.ts
export interface AppState {
todos: string[]
}
export enum AppAction {
CREATE_TODO = 'CREATE_TODO',
DELETE_TODO = 'DELETE_TODO',
}
export const initialState: AppState = {
todos: []
}
app.service.ts
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Manager } from '@dannymayer/vex'
import { Observable } from 'rxjs'
import { first, map, switchMap, withLatestFrom } from 'rxjs/operators'
import { AppAction, AppState } from './app.model'
@Injectable()
export class AppService {
constructor(
private _httpClient: HttpClient,
private _manager: Manager<AppState>,
) { }
// This method dispatches an asynchronous action. Notice that it uses the
// `resolve` function.
public createTodo(todo: string): Observable<AppState> {
return this._manager
.once({
type: AppAction.CREATE_TODO,
resolve: (state$) => this._httpClient.post('/api/todo', { todo }).pipe(
withLatestFrom(state$),
map(([response, state]) => ({
todos: [ ...state.todos, response ]
})),
),
})
.pipe(map(({ state }) => state))
}
// This method dispatches a synchronous action, and performs its asynchronous logic
// outside of the manager. Note that it uses the `reduce` function.
public deleteTodo(todoIndex: number): Observable<AppState> {
this._manager.dispatch({
type: AppAction.DELETE_TODO,
reduce: (state) => ({
...state,
todos: [
...state.todos.slice(0, todoIndex),
...state.todos.slice(todoIndex + 1),
],
}),
})
return this._httpClient.delete(`/api/todo/${todoIndex}`).pipe(
switchMap(() => this._manager.state$),
first(),
)
}
}
app.module.ts
import { HttpClientModule } from '@angular/common/http'
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { VexModule } from '@dannymayer/vex'
import { AppComponent } from './app.component'
import { initialState } from './app.model'
import { AppService } from './app.service'
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
VexModule.forRoot(initialState),
],
declarations: [ AppComponent ],
providers: [ AppService ],
bootstrap: [ AppComponent ],
})
export class AppModule { }