0.0.5 • Published 4 years ago

mini-rx-ng-devtools v0.0.5

Weekly downloads
1
License
MIT
Repository
github
Last release
4 years ago

npm version

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 state
    • select() to read feature state
    • createMiniEffect() 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 the effectFn you can access the payload$ Observable. That Observable emits as soon as the Effect is started (e.g. by calling deleteProductFn(123)). You can directly pipe on the payload$ 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 type Action. Or you can return this.SetStateAction...

    SetStateAction is available on the MiniFeature 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:

Redux Dev Tools for MiniRx

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):

Redux Dev Tools for MiniRx

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:

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.