mini-rx-ng-devtools v0.0.5
MiniRx: The Lightweight RxJS Redux Store
MiniRx Store provides Reactive State Management for Javascript Applications.
Attention: MiniRx is currently in beta phase. The API might still change.
If you have a bug or an idea, feel free to open an issue on GitHub.
Redux
MiniRx uses the Redux Pattern to make state management easy and predictable.
The Redux Pattern is based on this 3 key principles:
- Single source of truth (the Store)
- State is read-only and is only changed by dispatching actions
- Changes are made using pure functions called reducers
MiniRx Features
- Minimal configuration and setup
- MiniRx is lightweight - check the source code :)
- Advanced "Redux / NgRx Store" API: Although being a lightweight library, MiniRx supports many of the core features from the popular @ngrx/store library for Angular: Actions Reducers Memoized Selectors Effects
- Simplified API for basic state management per feature... You can update state without writing Actions and Reducers! This API operates directly on the feature state:
setState()
to update the feature stateselect()
to read feature statecreateMiniEffect()
create an effect with a minimum amount of code
- The source code is easy to understand if you know some RxJS :)
- RxJS is the one and only (peer) dependency
- Support for Redux Dev Tools
- Framework agnostic: Works with any front-end project built with JavaScript or TypeScript (Angular, React, Vue, or anything else)
When should you use MiniRx?
- If you have a small or medium sized application.
- If you tried to manage state yourself (e.g. with Observable Services) and you created state soup :)
- If you have the feeling that your app is not big / complex enough to justify a full-blown state management solution like NgRx then MiniRx is an easy choice.
Usage
Installation:
npm i mini-rx-store
Create the MiniStore (App State):
The MiniStore
is created and ready to use as soon as you import MiniStore.
import { MiniStore } from 'mini-rx-store';
Create a MiniFeature (Feature State):
A MiniFeature
holds a piece of state which belongs to a specific feature in your application (e.g. 'products', 'users').
The Feature States together form the App State (Single Source of Truth).
import { MiniStore } from 'mini-rx-store';
import { initialState, ProductState, reducer } from './state/product.reducer';
...
// Inside long living Module / Service
constructur() {
MiniStore.feature<ProductState>('products', initialState, reducer);
}
The code above creates a new feature state for products. Its initial state is set and the reducer function defines how the feature state changes along with an incoming Action.
Initial state example:
export const initialState: ProductState = {
showProductCode: true,
products: [],
};
A reducer function typically looks like this:
export function reducer(state: ProductState, action: ProductActions): ProductState {
switch (action.type) {
case ProductActionTypes.ToggleProductCode:
return {
...state,
showProductCode: action.payload
};
default:
return state;
}
}
Usually you would create a new MiniFeature
inside long living Modules/Services.
Create an Action:
export enum ProductActionTypes {
CreateProduct = '[Product] Create Product',
}
export class CreateProduct implements Action {
readonly type = ProductActionTypes.CreateProduct;
constructor(public payload: Product) { }
}
Dispatch an Action:
import { MiniStore } from 'mini-rx-store';
import { CreateProduct } from 'product.actions';
MiniStore.dispatch(new CreateProduct(product));
Write an effect:
Effects handle code that triggers side effects like API calls:
- An Effect listens for a specific Action
- That Action triggers the actual side effect
- The Effect needs to return a new Action
import { Action, actions$, ofType } from 'mini-rx-store';
import { LoadFail, LoadSuccess, ProductActionTypes } from './product.actions';
private loadProducts$: Observable<Action> = actions$.pipe(
ofType(ProductActionTypes.Load),
mergeMap(action =>
this.productService.getProducts().pipe(
map(products => (new LoadSuccess(products))),
catchError(err => of(new LoadFail(err)))
)
)
);
The code above creates an Effect. As soon as the Load
Action is dispatched the API call (this.productService.getProducts()
) will be executed. Depending on the result of the API call a new Action will be dispatched:
LoadSuccess
or LoadFail
.
You need to register the Effect before the corresponding Action is dispatched.
Register one or many effects:
MiniStore.effects([loadProducts$]);
Create Selectors:
Selectors are used to select and combine state.
import { createFeatureSelector, createSelector } from 'mini-rx-store';
const getProductFeatureState = createFeatureSelector('products');
export const getProducts = createSelector(
getProductFeatureState,
state => state.products
);
createSelector
creates a memoized selector. This improves performance especially if your selectors perform expensive computation.
If the selector is called with the same arguments again, it will just return the previously calculated result.
Select Observable State (with a selector):
import { MiniStore } from 'mini-rx-store';
import { getProducts } from '../../state';
this.products$ = MiniStore.select(getProducts);
select
runs the selector on the App State and returns an Observable which will emit as soon as the products data changes.
Make simple things simple - The MiniFeature
API
If a Feature in your application requires only simple state management, then you can fall back to a simplified API which is offered for each MiniFeature
instance (which is returned by the MiniStore.feature
function)
Get hold of the MiniFeature instance
private feature: MiniFeature<UserState> = MiniStore.feature<UserState>('users', initialState);
Alternatively you can extend MiniFeature:
export class ProductStateService extends MiniFeature<ProductState>{
constructor(
private productService: ProductService
) {
super('products', initialState, reducer);
}
}
The following examples use the extends
variant.
Select state with select
select(mapFn: ((state: S) => any)): Observable<any>
Example:
maskUserName$: Observable<boolean> = this.select(state => state.maskUserName);
select
takes a mapping function which gives you access to the current feature state (see the state
parameter).
Inside of that function you can pick a certain piece of state.
The returned Observable will emit the selected data over time.
Update state with setState
setState(stateFn: (state: S) => S): void
Example:
updateMaskUserName(maskUserName: boolean) {
this.setState((state) => {
return {
...state,
maskUserName
}
});
}
setState
takes also a mapping function which gives you access to the current feature state (see the state
parameter).
Inside of that function you can compose the new feature state.
Create an MiniEffect with createMiniEffect
createMiniEffect<PayLoadType = any>(
effectName: string,
effectFn: (payload: Observable<PayLoadType>) => Observable<Action>
): (payload?: PayLoadType) => void
Example:
deleteProductFn = this.createMiniEffect<number>(
'delete',
payload$ => payload$.pipe(
mergeMap((productId) => {
return this.productService.deleteProduct(productId).pipe(
map(() => new this.SetStateAction(state => {
return {
...state,
products: state.products.filter(product => product.id !== productId),
error: ''
}
})),
catchError(err => of(new this.SetStateAction(state => {
return {
...state,
error: err
};
})))
)
})
)
);
// Run the effect
deleteProductFn(123);
The code above creates a MiniEffect for deleting a product from the list. The API call this.productService.deleteProduct(productId)
is the side effect which needs to be performed.
createMiniEffect
returns a function which can be called later with an optional payload to start the MiniEffect (see deleteProductFn(123)
).
createMiniEffect
takes 2 arguments:
effectName: string
: ID which needs to be unique per feature. That ID will also show up in the logging (Redux Dev Tools / JS console).effectFn: (payload$: Observable<PayLoadType>) => Observable<Action>
: With theeffectFn
you can access thepayload$
Observable. That Observable emits as soon as the Effect is started (e.g. by callingdeleteProductFn(123)
). You can directlypipe
on thepayload$
Observable to access the payload value and do the usual RxJS things to run the actual Side Effect (mergeMap
,switchMap
etc).Also a MiniEffect needs to return a new Action as soon as the side effect did its job.
effectFn
needs to return that new Action. You can return any Action of typeAction
. Or you can returnthis.SetStateAction
...SetStateAction
is available on theMiniFeature
instance. Use it to update the feature state directly without creating any custom Actions. Its payload is a mapping function which gives you access to the current feature state. Inside of that function you can compose the new feature state.
FYI
Also the simplified API sets on Redux:
Behind the scenes MiniFeature
is creating a default reducer and a default action in order to update the feature state.
When you use setState()
or SetStateAction
MiniRx dispatches the default action and the default reducer will update the feature accordingly.
See the default Action in the Redux Dev Tools:
Settings
Enable Logging of Actions and State Changes in the Browser Console:
import { MiniStore } from 'mini-rx-store';
MiniStore.settings = {enableLogging: true};
The code above sets the global MiniStore Settings.
enableLogging
is currently the only available setting.
Typically you would set the settings when bootstrapping the app and before the store is used.
Redux Dev Tools (experimental):
MiniRx has basic support for the Redux Dev Tools (you can time travel and inspect the current state). You need to install the Browser Plugin to make it work.
Installation (Angular):
npm i mini-rx-ng-devtools
Add DevTools to Angular
import { NgReduxDevtoolsModule } from 'mini-rx-ng-devtools';
@NgModule({
imports: [
NgReduxDevtoolsModule
]
...
})
export class AppModule {}
If you do not use Angular
import { MiniStore, ReduxDevtoolsExtension } from 'mini-rx-store';
MiniStore.addExtension(new ReduxDevtoolsExtension());
Showcase
This Repo contains also an Angular showcase project.
Run npm i
Run ng serve mini-rx-store-showcase --open
to see MiniRx in action.
The showcase is based on the NgRx example from Deborah Kurata: https://github.com/DeborahK/Angular-NgRx-GettingStarted/tree/master/APM-Demo5
I did a refactor from NgRx to MiniRx and the app still works :)
References
These projects and articles helped and inspired me to create MiniRx:
- NgRx
- Observable Store
- RxJS Observable Store
- Basic State Managment with an Observable Service
- Redux From Scratch With Angular and RxJS
- How I wrote NgRx Store in 63 lines of code
TODO
- Further Integrate Redux Dev Tools
- Work on the ReadMe and Documentation
- Nice To Have: Test lib in React, Vue, maybe even AngularJS
- Add Unit Tests
License
MIT
Created By
If you like this, follow @spierala on twitter.