@animus-bi/redxs
v1.0.1
Published
A super lightweight redux implementation, inspired by NGXS that can run in React, Angular, Express, or anywhere!
Downloads
132
Maintainers
Readme
RedXS
RedXS is a super lightweight redux implementation, inspired by NGXS that can run anywhere! Asynchronous state management is a first class concept.
- Very simple composition
- Very little boilerplate
- Very easy to use and grasp
Concepts
- RedXS is basically just an async event bus. Actions are dispatched, which trigger any number of handlers. Handlers are attached via your store definition according to the Action
Type
they are attached to (No more large switch statements in reducers!!). Handlers given 2 parameters when executed:- Context to interact with the store's current state, which you can use to set/patch state or dispatch further actions.
- Available methods:
getState()
- returns the store's slice of state at the time it's called.
getRootState()
- returns the root store's state at the time it's called.
setState(obj: any)
- overwrites the store's slice of state
- sends new values through any
state$
androotState$
subscriptions
patchState(obj: Partial<any>)
- overwrites only the properties on
obj
on the store's slice of state - sends new values through any
state$
androotState$
subscriptions
- overwrites only the properties on
dispatch(action: any)
- dispatches actions (will trigger any/all handlers listening for the action).
- Available methods:
- The action that triggered the handler.
- should define a globally unique
type
orType
property. - may also have add'l properties attached.
- should define a globally unique
- Context to interact with the store's current state, which you can use to set/patch state or dispatch further actions.
Some Demo/Example Apps
Getting Started
Run
npm install --save @animus-bi/redxs
to install the redxs library.Define an Action
- An action can be any object.
- It should define a unique property
type
orType
- whether it's an instance property or a static/constructor property does not matter.
- in the absence of a
type
property, the constructor name is used
- It should define a unique property
- Here is an example action as a class:
// store/todo/actions.ts export const Intent = 'todo'; export class CreateTodo { static Type = `[${Intent}] Create A TODO`; constructor(public task: string) { } }
- Here is an example action as a function:
// store/todo/actions.ts export const Intent = 'todo'; export const CreateTodo = (task) => ({ type: `[${Intent}] Create A TODO`, task })
- You may have noticed the file name is just
actions.ts
- All actions for this intent will be exported from here.
- This allows them to be imported more easily as a contained set of actions
- All actions for this intent will be exported from here.
- You may have also noticed we're exporting an
Intent
for our actions.- This allows each handlers to execute in a more idempotent manner.
- An action can be any object.
Define a default state for your store
// store/todo/state.ts export class TodoState { list: any[] = []; hash: any = {}; }
Define a
Store
with aStoreConfig
// store/todo/store.ts export TodoStore = Store.Create<TodoState>( /* StoreConfig */ )
StoreConfig
is what drives theStore
implementation.- It establishes the state slice name in root state.
- It provides an initial state
- It allows you to attach handlers to specific actions that are dispatched.
- An example of a
StoreConfig
using an object literal// store/todo/store.ts import * as TodosActions from './todos.actions' export TodoStore = Store.Create<TodoState>({ name: TodosActions.Intent, initialState: new TodoState(), handlers: { } });
- An example of a
StoreConfig
using the staticStoreConfig.create
method// store/todo/store.ts import { Store, StoreConfig } from '@animus-bi/redxs'; import * as TodosActions from './actions'; import { TodoState } from './state'; const storeConfig = StoreConfig.create( TodosActions.Intent, new TodoState(), { } ); export TodoStore = Store.Create<TodoState>(storeConfig);
You may have noticed, the
Intent
for a set of actions has become the name of our store, tying together our set of actions with our slice of application state.- A
Store
instance has the following methods/properties:state$: Observable<any>
- returns an observable of the current store's slice of state.
- new values are piped through any time
setState()
orpatchState()
are called
rootState$: Observable<any>
- returns an observable of the root store's state.
- new values are piped through any time
setState()
orpatchState()
are called
currentState(): any
- returns an instance of the store's state slice at a given point in time (when called)
currentRootState(): any
- returns an instance of the root store's state at a given point in time (when called).
dispatch(action: any|{type: string}): Observable<void>
- returns an observable of type void
- triggers any action handlers registered in any other stores, matching the dispatched actions's
Type
ortype
.
Now we can wire up some handlers in our store so that we can do things when Actions are dispatched.
Handlers are not called directly by your code; instead, they are invoked when an Action of a matching
Type
is dispatched.Each handler is passed 2 arguments:
- A
StateContext<T>
, which provides some operations to interact with state at the time the handler is executed. - The dispatched Action that triggered it.
// store/todo/store.ts import { Store, StateContext, StoreConfig } from '@animus-bi/redxs'; import * as TodosActions from './actions'; import { TodoState } from './state'; const createTodo = (ctx: StateContext<TodoState>, action: TodosActions.CreateTodo) => { const { list, hash } = ctx.getState(); list.push(action.payload); hash[action.payload.task] = action.payload; return ctx.patchState({ list, hash }); } export TodoStore = Store.Create<TodoState>({ /* ... */ });
- A
In order that the
createTodo
function is called when the Action is dispatched, we must add it to our store's StateConfig so that our Actiontype
is the key name for the handler.Note: you are not calling the handler in the config, but rather, you're passing a reference to the handler. To avoid any lexical problems, use
.bind(this)
// store/todo.ts import { Store, StateContext, StoreConfig } from '@animus-bi/redxs'; import * as TodosActions from './actions'; import { TodoState } from './state'; const createTodo = (ctx: StateContext<TodoState>, action: TodosActions.CreateTodo) => { const { list, hash } = ctx.getState(); list.push(action.payload); hash[action.payload.task] = action.payload; return ctx.patchState({ list, hash }); } export TodoStore = Store.Create<TodoState>({ name: TodosActions.Intent, initialState: new TodoState(), handlers: { [TodosActions.CreateTodo.Type]: createTodo.bind(this) } });
You may also attach multiple handlers to a single dispatched action:
// store/todo.ts import { Store, StateContext, StoreConfig } from '@animus-bi/redxs'; import * as TodosActions from './actions'; import { TodoState } from './state'; const preCreateTodo = (ctx: StateContext<TodoState>, action: TodosActions.CreateTodo) => { console.log('intend to create a todo'); } const createTodo = (ctx: StateContext<TodoState>, action: TodosActions.CreateTodo) => { const { list, hash } = ctx.getState(); list.push(action.payload); hash[action.payload.task] = action.payload; return ctx.patchState({ list, hash }); } export TodoStore = Store.Create<TodoState>({ name: TodosActions.Intent, initialState: new TodoState(), handlers: { // // Good/Ok // [TodosActions.CreateTodo.Type]: [ preCreateTodo.bind(this) createTodo.bind(this) ] } });
You should NOT list the same key twice in a store's action handler config (this is standard js stuff).
- You could do this, but the last one will probably win, and the other may not fire at all. Just use the one key with an array of handlers
// store/todo.ts import { Store, StateContext, StoreConfig } from '@animus-bi/redxs'; import * as TodosActions from './actions'; import { TodoState } from './state'; const preCreateTodo = (ctx: StateContext<TodoState>, action: TodosActions.CreateTodo) => { console.log('intend to create a todo'); } const createTodo = (ctx: StateContext<TodoState>, action: TodosActions.CreateTodo) => { const { list, hash } = ctx.getState(); list.push(action.payload); hash[action.payload.task] = action.payload; return ctx.patchState({ list, hash }); } export TodoStore = Store.Create<TodoState>({ name: TodosActions.Intent, initialState: new TodoState(), handlers: { // // !!!!!!BAD!!!!!!!!!!!!!! // [TodosActions.CreateTodo.Type]: preCreateTodo.bind(this), [TodosActions.CreateTodo.Type]: createTodo.bind(this) } });
To access state in anything, simply subscribe to your store's
state$
property wherever you want to receive state updates.import { TodoStore } from '../store/todo'; export class SomeComponent { constructor() { this.subscription = TodoStore.state$.subscribe((todoState) => { this.todoState = todoState; }) } }
- It is often useful to set a default state value in your component.
- To do that, call your store's
currentState()
method, which will return the current state of your slice (NOT NECESSARILY INITIAL STATE).- This ensures resilience through re-render, initial loading, and late/lazy loading alike.
- JavaScript
import { TodoStore } from '../store/todo'; export class SomeComponent { constructor() { this.todoState = TodoStore.currentState(); this.subscription = TodoStore.state$.subscribe((todoState) => { this.todoState = todoState; }) } }
- TypeScript
import { TodoStore } from '../store/todo'; export class SomeComponent { todoState = TodoStore.currentState(); constructor() { this.subscription = TodoStore.state$.subscribe((todoState) => { this.todoState = todoState; }); } }
- To do that, call your store's
- It is often useful to set a default state value in your component.
Dispatch Actions from anywhere
- React example
import { TodoStore } from '../store/todo'; import * as TodosActions from './store/todo/actions'; export class SomeComponent { addTodo(_e) { const text = document.getElementById('todo-input').value store.dispatch(new TodosActions.CreateTodo(text)); } render() { return <div> <input type="text" id="todo-input" value="" /> <button onClick={this.addTodo}>add todo</button> </div> } }
- Angular example
import { Component } from '@angular/core'; import { TodoStore } from '../store/todo'; import * as TodosActions from './store/todo/actions'; @Component({ selector: "some-component", template: ` <div> <input type="text" id="todo-input" value="" /> <button (click)="addTodo()">add todo</button> </div> `}) export class SomeComponent { addTodo() { const text = document.getElementById('todo-input').value store.dispatch(new TodosActions.CreateTodo(text)); } }
Combine dispatching and subscribing as needed for an overall async pub/sub model. See examples above for more info.