DEV Community

Cover image for Getting started with Angular Effects
Michael Muscat
Michael Muscat

Posted on • Edited on

Getting started with Angular Effects

Angular Effects is a reactive state management solution for Angular. This article explains the initial setup and basic process for adding effects to your application.


This is part II in a series on Reactive State in Angular. Read Part I: Introducing Angular Effects


Installation

Angular Effects is available on NPM.

npm install ng-effects

Alternatively, you can download the source from Github.

Peer Dependencies

Angular Effects is compatible with Angular 9.0.0+ and RxJS 6.5+.

Usage

Effects are defined by annotating component methods with the @Effect() decorator.

@Component()
export class AppComponent {
    @Effect()
    myAwesomeEffect() {
        // Return an observable, subscription or teardown logic
    }
}

The example above is the minimum code necessary for a valid effect, but it won't do anything until we connect it.

Host effects and effect services

You can define effects on any component, directive or module. For brevity I will refer to these collectively as components. Effects can also be defined in injectable services.

@Injectable()
export class AppEffects {
    @Effect()
    myAwesomeEffect() {
        // Return an observable, subscription or teardown logic
    }
}

Effects defined directly on components are referred to as "host" effects, whereas services that provide effects are referred to as "effect services". This distinction is important when connecting effects.

Connecting effects

For every component we want to run effects on, there is some wiring involved.

First we must provide the Effects token in the providers array for each component that has effects.

@Component({
    providers: [Effects]
})

By providing this token the component can now be "connected". Also add any effect services that should be connected.

@Component({
    providers: [Effects, AppEffects]
})

The next step is to inject the Connect function and call it from the constructor of the component.

@Component({
    providers: [Effects, AppEffects]
})
export class AppComponent {
    author?: Author
    books: Book[]

    constructor(connect: Connect) {
        this.books = []          // Should initialize variables
        this.author = undefined  // even if they are undefined.

        connect(this)            // Must always be called in the constructor
    }

    @Effect()
    myAwesomeEffect() {
        // Return an observable, subscription or teardown logic
    }
}

NOTE: connect() should be called after initializing class fields with default values.

As seen here, components can utilise both host effects and effect services at the same time. Mix and match as you see fit.

Anatomy of an effect

Now that we know how to create and initialize effects in our components, it's time to explore what goes inside. Each effect method is a factory that is only called once, each time the component is created. What we do inside each effect should therefore take into account the entire lifecycle of a component.

Depending on the configuration, the effect will either run:

  • the moment connect() is called; OR
  • immediately after the first change detection cycle (ie. when it has rendered).

The behaviour of each effect depends on its configuration and return value.

Arguments

For convenience, each effect receives three arguments. The same values can also be obtained by injecting HostRef<T> through the constructor.

Argument Type Description
state State<T> An object map of observable fields from the connected component.

The state object is the mechanism by which we can observe when a property on the component changes. There are two behaviors that should be observed before using it.

@Component()
export class AppComponent {
    count = 0

    @Effect()
    myAwesomeEffect(state: State<AppComponent>) {
        return state.count.subscribe(value => console.log(value))
    }
}
Output:
> 0

When subscribing to a property, the current state is emitted immediately. The value is derived from a BehaviorSubject, and is read only.

@Component()
export class AppComponent {
    count = 0

    @Effect()
    myAwesomeEffect(state: State<AppComponent>) {
        return state.count.subscribe(value => console.log(value))
    }

    @Effect("count")
    setCount() {
        return from([0, 0, 0, 10, 20])
    }
}
Output:
> 0
> 10
> 20

You might expect 0 to be logged several times, but here it's only logged once as state only emits distinct values.

Keep this in mind when writing effects. Helpful error messages will be shown when trying to access properties that cannot be observed (ie. they are missing an initializer or are not enumerable).

Argument Type Description
context Context<T> A reference to the component instance.

The second argument is the component instance itself. There are times when we want to simply read the current value of a property, invoke a method or subscribe to a value without unwrapping it from state first.

interface AppComponent {
    formData: FormGroup
    formChange: EventEmitter
}

@Injectable()
export class AppEffects {
    @Effect()
    myAwesomeEffect(state: State<AppComponent>, context: Context<AppComponent>) {
        return context
            .formData
            .valueChanges
            .subscribe(context.formChange)
    }
}

Effects can be used in a variety of ways, from a variety of sources. Angular Effects lets us compose them as we see fit.

Argument Type Description
observer Observable<T> An observable that is similar to DoCheck.

The last argument is one that should rarely be needed, if ever. It emits once per change detection cycle, as well as whenever an effect in the current context emits a value. Use this observable to perform custom change detection logic, or debug the application.

Return values

Unless modified by an adapter, each effect must return either an observable, a subscription, a teardown function, or void. The return value dictates the behavior and semantics of the effects we write.

  • Effect -> Observable

When we want to bind the emissions of an effect to one or more properties on the connected component, we do so by returning an observable stream.

@Component()
export class AppComponent {
    count = 0

    @Effect("count")
    incrementCount(state: State<AppComponent>) {
        return state.count.pipe(
            take(1),
            increment(1),
            repeatInterval(1000)
        )
    }
}

We can return observables for other reasons too, such as scheduling change detection independent of values changing, or when using adapters.

  • Effect -> Subscription

The semantics of returning a subscription is to perform side effects that do not affect the state of the component. For example, dispatching an action.

@Component()
export class AppComponent {
    count = 0

    @Effect()
    dispatchCount(state: State<AppComponent>) {
        return state.count.subscribe(count => {
            this.store.dispatch({
                type: "COUNT_CHANGED",
                payload: count
            })
        })
    }

    constructor(private store: Store<AppState>) {}
}

TIP: While good enough to illustrate this particular example, later we will see better ways to integrate global state patterns using effect adapters.

  • Effect -> Teardown function

Angular Effects can be written in imperative style as well. This is particularly useful when doing DOM manipulation.

@Component()
export class AppComponent {
    @Effect({ whenRendered: true })
    mountDOM(state: State<AppComponent>) {
        const instance = new MyAwesomeDOMLib(this.elementRef.nativeElement)

        return () => {
            instance.destroy()
        }
    }

    constructor(private elementRef: ElementRef) {}
}
  • Effect -> void

If nothing is returned, it is assumed we are performing a one time side effect that does not require any cleanup.

Configuration

The last part of the effect definition is the metadata passed to the decorator.

@Component()
export class AppComponent {
    @Effect({
        bind: undefined,
        assign: undefined,
        markDirty: undefined,
        detectChanges: undefined,
        whenRendered: false,
        adapter: undefined
    })
    myAwesomeEffect() {}
}

Each option is described in the table below.

Option Type Description
bind string When configured, maps values emitted by the effect to a property of the same name on the host context. Throws an error when trying to bind to an uninitialised property. Default: undefined
assign boolean When configured, assigns the properties of partial objects emitted by the effect to matching properties on the host context. Throws an error when trying to bind to any uninitialised properties. Default: undefined
markDirty boolean When set to true, schedule change detection to run whenever a bound effect emits a value. Default: true if bind or apply is set. Otherwise undefined
detectChanges boolean When set to true, detect changes immediately whenever a bound effect emits a value. Takes precendence over markDirty. Default: undefined
whenRendered boolean When set to true, the effect deferred until the host element has been mounted to the DOM. Default: false
adapter Type Hook into effects with a custom effect adapter. For example, dispatching actions to NgRx or other global state stores. Default: undefined

We'll explore these options and more in future articles.

You already know how to write effects

If you're using observables and connecting them to async pipes in your template, then you already know how to use this library. Angular Effects are easier to write, and even easier to use. It's type safe. It's self managed. It lets components focus on the things they are good at: rendering views and dispatching events.

TIP: Angular Effects can also be used to compose global effects with component scope (including route components). Don't forget that it works with modules and directives too.

Next time we'll look at how some common Angular APIs can be adapted to work with Angular Effects for fun and for profit.

Thanks for reading!

Next in this series

Top comments (1)

Collapse
 
oleksandr profile image
Oleksandr

Nice approach!
Are you on Twitter?