Skip to content

Latest commit

 

History

History
231 lines (170 loc) · 6.85 KB

README.md

File metadata and controls

231 lines (170 loc) · 6.85 KB

Vex

Vex is a simple, lightweight, asynchronous state manager for JavaScript user interfaces.

Installation

npm install @dannymayer/vex

API Overview

Manager<StateType>

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 an actionType is provided, filters the returned Observable to only emit dispatches of that actionType.

results(actionType?: string): Observable<ActionResult<StateType>>
Returns an Observable that emits each time an action is resolved. If an actionType is provided, filters the returned Observable to only emit results of that actionType.

createManager<StateType>

parameter initialState: StateType
(required) Each manager must be initialized with an initialState.

parameter options?: ManagerOptions (optional)

ManagerOptions

allowConcurrency: boolean
(optional) Defaults to true; if set to false, an Action dispatched before the previous Action has resolved will be queued and executed immediately when the previous Action resolves (using RxJS's concatMap).

Action<StateType>

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.

ActionResult<StateType>

state: StateType
(required) A snapshot of the state at the time the ActionResult was created.

actionType: string (required)

error?: any (optional)

Configuring Redux DevTools

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())
  }
}

DevToolsOptions

(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

Background

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.

"To Do" App

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 { }