@animus-bi/redxs v1.0.1
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
Typethey are attached to (No more large switch statements in reducers!!). Handlers given 2 parameters when executed: 1) 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 anystate$androotState$subscriptions -patchState(obj: Partial<any>)- overwrites only the properties onobjon the store's slice of state - sends new values through anystate$androotState$subscriptions -dispatch(action: any)- dispatches actions (will trigger any/all handlers listening for the action). 2) The action that triggered the handler. - should define a globally uniquetypeorTypeproperty. - may also have add'l properties attached.
Some Demo/Example Apps
Getting Started
Run
npm install --save @animus-bi/redxsto install the redxs library.Define an Action
- An action can be any object.
- It should define a unique property
typeorType- whether it's an instance property or a static/constructor property does not matter.
- in the absence of a
typeproperty, 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
Intentfor 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
Storewith aStoreConfig// store/todo/store.ts export TodoStore = Store.Create<TodoState>( /* StoreConfig */ )StoreConfigis what drives theStoreimplementation.- 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
StoreConfigusing 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
StoreConfigusing the staticStoreConfig.createmethod```ts // 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
Intentfor a set of actions has become the name of our store, tying together our set of actions with our slice of application state.A
Storeinstance 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
Typeortype.
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
Typeis 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
createTodofunction is called when the Action is dispatched, we must add it to our store's StateConfig so that our Actiontypeis 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) } });
- Handlers are not called directly by your code; instead, they are invoked when an Action of a matching
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
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.
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago