Giter Club home page Giter Club logo

ngxs-effects's Introduction

Ngxs-Effects

Build Status Coverage Status

Goal of ngxs-effects

The main goal is to gracefully separate the effects logic from the definition of the repository itself. Creating effects should be easy without worrying about managing your subscriptions and unsubscribes. Just mark how you would like to react to certain events in your store. We will do the rest for you.

Quick links

Features

  • Separated effects from ngxs store definition
  • Application-wide effect / feature-wide effects
  • Application-lifetime effects / feature-lifetime effects
  • Decorators for for effects logic and effects initialization and termination
  • Custom error handlers
  • Limit lifetime effects

Installation

npm i -S ngxs-effects

Quick start

First, define your storage. This is a common repository like you used to create

@State<Record<string, PonyInterface>>({
    name: 'state',
})
@Injectable()
class PonyState {
    @Action(AddPony)
    addPony(context: StateContext<Record<string, PonyInterface>>, {payload}: AddPony): void {
        context.setState({ ...context.getState(), [payload.id]: payload });
    }

    @Action(RemovePony)
    removePony(context: StateContext<Record<string, PonyInterface>>, {payload}: RemovePony): void {
        context.setState({ ...context.getState(), [payload.id]: null });
    }
}

Next, we create a separate service dealing with effects. Suppose we would like to output to the console what was transmitted in one of the actions. To do this, we need to mark one of the methods as a decorator @Effect

@Injectable()
class PonyEffectsService {
    @Effect(AddPony)
    logAddPony({payload}: AddPony): void {
        console.log(payload)
    }
}

Next, add a core effects module to the imports of our root module. And also declare which service is used to process effects.

@NgModule({
    imports: [
        // ...
        NgxsEffectsModule.forRoot(),
        NgxsEffectsModule.forFeature(PonyEffectsService),
    ],
    // ...
})
export class AppModule {}

And it's all! Now we have a dedicated effects service

But what if we add handling actions that should return observable. For example, we want to send a request to the server. It is easy! Just return it from the method marked by the decorator

@Injectable()
class PonyEffectsService {
    constructor(private httpClient: HttpClient) {}

    @Effect(AddPony)
    notifyAboutNewPony$({payload}: AddPony): Observable<PonyInterface> {
        return this.httpClient.post<PonyInterface>('my/api/pony', { body: payload })
    }
}

Start effects

By default, effects begin to listen to actions from the moment they are declared in the NgxsEffectsModule module. But suppose we would like to start wiretapping at a specific point in time. For example, when the repository appears in our place lazily in a lazily loaded module or component. In this case, it would be convenient for us to start processing actions along with calling a certain method in the effects service

We can achieve this behavior using a decorator @EffectsStart

@Injectable()
class PonyEffectsService {
    constructor(private httpClient: HttpClient) {}

    @EffectsStart()
    onStart(): void {
        console.log('PonyEffects started...');
    }

    @Effect(AddPony)
    notifyAboutNewPony$({payload}: AddPony): Observable<PonyInterface> {
        return this.httpClient.post<PonyInterface>('my/api/pony', { body: payload })
    }
}

Now in the lazily loaded component we will determine when the effects begin

@Component({
    selector: 'app-pony-list',
    templateUrl: './pony-list.component.html',
    styleUrls: ['./pony-list.component.sass'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PonyListComponent implements OnInit {
    constructor(private ponyEffectsService: PonyEffectsService) {}

    ngOnInit(): void {
        this.ponyEffectsService.onStart();
    }
}

Now processing for events in this service will work only with the moment this component appears. In addition, we can now transfer the definition of our effects for features to the module of this component

@NgModule({
    imports: [
        // ...
        NgxsEffectsModule.forFeature(PonyEffectsService),
    ],
    declarations: [
        PonyListComponent,
    ]
})
export class PonyListModule {}

It is important to note that with repeated appearances of this component we will not duplicate the processing of this effect

Terminate effects

Now that we know when the effect sampling begins, we would like to complete it at a certain moment, for example, when the component disappears from view

We can achieve this behavior using a decorator @EffectsTerminate

@Injectable()
class PonyEffectsService {
    constructor(private httpClient: HttpClient) {}

    @EffectsStart()
    onStart(): void {
        console.log('PonyEffects started...');
    }

    @Effect(AddPony)
    notifyAboutNewPony$({payload}: AddPony): Observable<PonyInterface> {
        return this.httpClient.post<PonyInterface>('my/api/pony', { body: payload })
    }

    @EffectsTerminate()
    onTerminate(): void {
        console.log('PonyEffects terminated...');
    }
}

Now in the lazily loaded component we will determine when the effects begin

@Component({
    selector: 'app-pony-list',
    templateUrl: './pony-list.component.html',
    styleUrls: ['./pony-list.component.sass'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PonyListComponent implements OnInit, OnDestroy {
    constructor(private ponyEffectsService: PonyEffectsService) {}

    ngOnInit(): void {
        this.ponyEffectsService.onStart();
    }

    ngOnDestroy(): void {
        this.ponyEffectsService.onTerminate();
    }
}

Error handling

Periodically, effects can lead to errors. An error that can happen during the operation of the effect will stop the processing of actions on the method where it occurred. Other effects in the same service continue to work. In order to correctly handle these errors in the same class where the effect is declared, we need a special tool

For that reasons you can use EffectsCatchError

@Injectable()
class PonyEffectsService {

    // ...

    @EffectsCatchError()
    onError(error: Error): void {
        console.log('PonyEffects error', error);
    }
}

Global error handling

In order to catch errors occurring in all the effects services you can define your EFFECTS_ERROR_HANDLER

First you need to implement EffectErrorHandlerInterface

@Injectable({
    providedIn: 'root',
})
class CustomEffectErrorHandler implements EffectErrorHandlerInterface {
    onError(error: Error): void {
        console.log(error);
    }
}

Next uoy need to provide this services with EFFECTS_ERROR_HANDLER token

@NgModule({
    providers: [
        // ...
        { provide: EFFECTS_ERROR_HANDLER, useExisting: CustomEffectErrorHandler }
    ],
    // ...
})
export class AppModule {}

Contributing and development

Start

Install project

git clone https://github.com/vladborsh/ngxs-effects.git
npm i
npm run build

Development server

Run npm run start for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.

Build

Run npm run build to build the project. The build artifacts will be stored in the dist/ directory

Running unit tests

Run npm run test to execute the unit tests via Karma.

ngxs-effects's People

Contributors

dependabot[bot] avatar vladborsh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

ngxs-effects's Issues

Compatibility issue with 'ng xi18n'

When I try to extract i18n messages from source code, I get an error:

ERROR in Encountered undefined provider! Usually this means you have a circular dependencies. This might be caused by using 'barrel' index.ts files.

I found out that this error rise with NgxsEffectsModule.forRoot()

import { NgModule } from '@angular/core';
import { NgxsModule } from '@ngxs/store';
import { NgxsEffectsModule } from 'ngxs-effects';

@NgModule({
  imports: [NgxsModule.forRoot(), NgxsEffectsModule.forRoot()],
})
export class StoreModule {}

Run the command ng xi18n and get the error.

Cannot be used with latest Angular

Using the latest version of ngxs-effects (3.1.3) and Nodejs 18.10.0, it is not possible to complete NPM install successfully with Angular 16.1.9 (and potentially recent versions of Angular such as v15) due to dependency issues.

ngxs-effects has a dependency on @ngxs/logger-plugin version ~3.6.2 but Angular 16 requires @ngxs/logger-plugin version 3.8.1.

Changing the ~ to a ^ may resolve the issue if ngxs-effects 3.1.3 is compatible with @ngxs/logger-plugin 3.8.1

Here is console output when trying to install:

image

Allow decoration of methods with no arguments

Please consider this example:

export class DoSomething {
  static readonly type = '[DO SOMETHING]';
}

@Injectable()
export class SomeEffectsClass {
  @Effect(DoSomething)
  doSomething() {
    // Just do something
  }
}

In this case, I get a Typescript compiler error:

Argument of type 'TypedPropertyDescriptor<() => void>' is not assignable to parameter of type 'TypedPropertyDescriptor<(args: DoSomething) => unknown>'.
  Types of property 'set' are incompatible.
    Type '((value: () => void) => void) | undefined' is not assignable to type '((value: (args: DoSomething) => unknown) => void) | undefined'.
      Type '(value: () => void) => void' is not assignable to type '(value: (args: DoSomething) => unknown) => void'.
        Types of parameters 'value' and 'value' are incompatible.ts(2345)

This error can be fixed by adding a parameter to the doSomething method:

export class DoSomething {
    static readonly type = '[DO SOMETHING]';
  }

@Injectable()
export class SomeEffectsClass {
  @Effect(DoSomething)
  doSomething(_: DoSomething) {
    // Just do something
  }
}

However, it does not really make much sense to add a parameter that is not actually used inside the function. Besides, in my case this violates the https://eslint.org/docs/rules/no-unused-vars ESLint rule that I use to make sure no unused function params are declared.

Of course, the eslint warning can be suppressed for each such usage (or it could be disabled globally), but that is suboptimal.

It would be great if the Effects decorator just supported functions with no arguments.

Typescript version: 4.3.5

Metadata symbols

Actual behavior

At the moment there is an ability to extract/write metadata by direct access with key

  • Fix the ability to rewrite metadata by string-key in target constructor
  • Use Symbol instead of enums value as a metadata key
  • Cover these cases (metadata rewriting by direct access to target constructor) with unit tests

Ability to pass multiple actions

Feature request

Ability to pass multiple actions in one decorator instead of multiple decorators on one method

Actual behavior

@Effect(CreateSomthing)
@Effect(UpdateIt)
@Effect(DeleteOther)
public doChanges(): void {
    this.service.setChanges(true);
}

Excepted behavior

@Effect(CreateSomthing, UpdateIt, DeleteOther)
public doChanges(): void {
    this.service.setChanges(true);
}

Angular 9 Ivy compiler issue

I'm excited to see that someone got around to adding this plugin; however, when I compile it in my Angular 9 Ivy project, I get this error:

    740     static ɵfac: ɵngcc0.ɵɵFactoryDef<Compiler, never>;
                                ~~~~~~~~~~~~
    node_modules/ngxs-effects/node_modules/@angular/core/core.d.ts:4730:25 - error TS2694: Namespace '"<omitted>node_modules/ngxs-effects/node_modules/@angular/core/src/r3_symbols"' has no exported member 'ɵɵFactoryDef'.

    4730     static ɵfac: ɵngcc0.ɵɵFactoryDef<PlatformRef, never>;
                                 ~~~~~~~~~~~~
    node_modules/ngxs-effects/node_modules/@angular/core/core.d.ts:6207:25 - error TS2694: Namespace '"node_modules/ngxs-effects/node_modules/@angular/core/src/r3_symbols"' has no exported member 'ɵɵFactoryDef'.

    6207     static ɵfac: ɵngcc0.ɵɵFactoryDef<SystemJsNgModuleLoader, [null, { optional: true; }]>;
                                 ~~~~~~~~~~~~
    node_modules/ngxs-effects/node_modules/@angular/core/core.d.ts:6397:25 - error TS2694: Namespace '"<omitted>node_modules/ngxs-effects/node_modules/@angular/core/src/r3_symbols"' has no exported member 'ɵɵFactoryDef'.

    6397     static ɵfac: ɵngcc0.ɵɵFactoryDef<Testability, never>;
                                 ~~~~~~~~~~~~
    node_modules/ngxs-effects/node_modules/@angular/core/core.d.ts:6442:25 - error TS2694: Namespace '"<omitted>node_modules/ngxs-effects/node_modules/@angular/core/src/r3_symbols"' has no exported member 'ɵɵFactoryDef'.

    6442     static ɵfac: ɵngcc0.ɵɵFactoryDef<TestabilityRegistry, never>;
                                 ~~~~~~~~~~~~
    node_modules/ngxs-effects/node_modules/@angular/core/core.d.ts:9407:25 - error TS2694: Namespace '"<omitted>node_modules/ngxs-effects/node_modules/@angular/core/src/r3_symbols"' has no exported member 'ɵɵFactoryDef'.

    9407     static ɵfac: ɵngcc0.ɵɵFactoryDef<ɵConsole, never>;
                                 ~~~~~~~~~~~~
    node_modules/ngxs-effects/dist/ngxs-effect/lib/effect-starter.service.d.ts:16:25 - error TS2694: Namespace '"<omitted>node_modules/ngxs-effects/node_modules/@angular/core/core"' has no exported member 'ɵɵFactoryDef'.

    16     static ɵfac: ɵngcc0.ɵɵFactoryDef<EffectStarterService, [null, { optional: true; }, { optional: true; }]>;

Fix linter warning

Actual behavior

Next warning appears during running linter check

Could not find implementations for the following rules specified in the configuration:
    component-class-suffix
    component-selector
    contextual-lifecycle
    directive-class-suffix
    directive-selector
    no-conflicting-lifecycle
    no-host-metadata-property
    no-input-rename
    no-inputs-metadata-property
    no-output-native
    no-output-on-prefix
    no-output-rename
    no-outputs-metadata-property
    template-banana-in-box
    template-no-negated-async
    use-lifecycle-interface
    use-pipe-transform-interface

Expected behavior

No warning during lint check

Desired fix

Check rule names changes in codelizer

ngxs-effects not compatible with Angular 10

When upgrading to angular 10, I receive this error when running my application:

Error: inject() must be called from an injection context at injectInjectorOnly (core.js:934) at Module.ɵɵinject (core.js:950) at Object.NgxsEffectsModule_Factory [as factory] (ngxs-effect.umd.js:465) at R3Injector.hydrate (core.js:11266) at R3Injector.get (core.js:11088) at core.js:11125 at Set.forEach (<anonymous>) at R3Injector._resolveInjectorDefTypes (core.js:11125) at new NgModuleRef$1 (core.js:23975) at NgModuleFactory$1.create (core.js:24029)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.