/reactive-state

Redux-like state management using reactive RxJS in TypeScript. Wrist-friendly, without any boilerplate or endless switch statements

Primary LanguageTypeScriptMIT LicenseMIT

Build Status npm version code coverage

Reactive State

A typed, wrist-friendly state container aimed as an alternative to Redux when using RxJS. Written in TypeScript but usable from plain JavaScript. Originally inspired by the blog posting from Michael Zalecki but heavily modified and extended since.

Features

  • wrist-friendly with no boilerplate code, no string constants, and not a single switch statement
  • Actions are just Observables, so are Subjects. Call .next() to dispatch an action.
  • dynamically add and remove reducers during runtime (usefull in lazy-loaded application modules)
  • no need for async middlewares such as redux-thunk/redux-saga; actions are Observables and can be composed and transformed asynchronously leveraging RxJS built-in operators
  • single, application-wide Store concept as in Redux, but with linked standalone stores representing slices/substates for easy reducer composition and sub-tree notifications
  • Strictly typed to find errors during compile time
  • Heavily tested, 70+ tests for ~150 lines of code

Installation

npm install --save reactive-state

Documentation

Additionally, there is a small example.ts file and see also see the included unit tests as well.

Example Usage

import { Store, Reducer, Action } from "reactive-state";

// The main (root) state for our example app
interface AppState {
    counter: number;
}

const initialState: AppState = {
    counter: 0
}

const store = Store.create(initialState);

// The .select() function returns an Observable that emits every state change; we can subscribe to it
// the second argument true will - for the sake of this example force output - every state change even
// to nested properties
store.select(state => state, true).subscribe(newState => console.log("ROOT STATE:", JSON.stringify(newState)));

// the state Observable always caches the last emitted state, so we will immediately get printed the inital state:
// [CONSOLE.LOG] ROOT STATE: {"counter":0}

// Actions are just extended RxJS Subjects
const incrementAction = new Action<number>();
const incrementReducer: Reducer<AppState, number> = (state, payload) => {
    return { ...state, counter: state.counter + payload };
};

// register reducer for an action
const incrementSubscription = store.addReducer(incrementAction, incrementReducer);

// dispatch actions

incrementAction.next(1);
// [CONSOLE.LOG]: ROOT STATE: {"counter":1}
incrementAction.next(1);
// [CONSOLE.LOG]: ROOT STATE: {"counter":2}

// reducers can be unsubscribed dynamically - that means they won't react to the action anymore
incrementSubscription.unsubscribe();

// Now, here is the more powerfull part of Reactive State: lets use a slice to simplifiy our code!

const sliceStore = store.createSlice("counter");
// Note: while the first argument "counter" above may look like a magic string it is not: it is
// of type "keyof Appstate"; using any other string that is not a valid property name of AppState will thus
// trigger a TypeScript compilation error. This make it safe for refactorings :)

const incrementSliceReducer: Reducer<number, number> = (state, payload) => state + payload;
sliceStore.addReducer(incrementAction, incrementSliceReducer);

sliceStore.select().subscribe(counter => console.log("COUNTER STATE:", counter));
// [CONSOLE.LOG] COUNTER STATE: 2

incrementAction.next(1);
// [CONSOLE.LOG] ROOT STATE: {"counter":3}
// [CONSOLE.LOG] COUNTER STATE: 3

incrementAction.next(1);
// [CONSOLE.LOG] ROOT STATE: {"counter":4}
// [CONSOLE.LOG] COUNTER STATE: 4

// Note how the ROOT STATE change subscription still is active; even if we operate on a slice, it is still
// linked to a single root store. The slice is just a "view" on the state, and replace reducer composition.

License

MIT.