API Reference

For brevity, this document only refers to components. Most of these APIs can be applied to directives and services as well.

defineComponent

alias: defineDirective.

The defineComponent function is the main entry point for using the Composition API inside components.

  • Usage with Templates

If the factory passed to defineComponent returns an object, the properties on the object will be merged on to the class instance for the component’s template:

import { Component, Injector } from "@angular/core"
import { defineComponent } from "ng-effects"

@Component()
export class NgComponent extends defineComponent(setup) {}

function setup() {
    const count = ref(0)
    const object = reactive({ foo: "bar" })

    // expose to template
    return {
        count,
        object
    }
}

Note that refs returned from setup are automatically unwrapped when accessed in the template so there’s no need for .value in templates.

  • Arguments

defineComponent takes an optional factory function that receives no arguments.

declare function defineComponent<T>(factory?: () => T | void): Type<T>
  • Metadata

Angular uses class metadata to setup host bindings such as inputs, outputs, queries and host bindings/listeners. These are usually declared in the class using decorators.

@Component({
    template: `<input #ref />`
})
export class NgComponent {
    @Input()
    name: string = ""

    @Output()
    nameChange: EventEmitter<string> = new EventEmitter()

    @ViewChild("ref")
    input?: HTMLInputElement

    @HostListener("click")
    handleClick() {}

    @HostBinding("class.active")
    active: boolean = false
}

In Angular Effects it is recommended to move these annotations into the @Component metadata literal.

@Component({
    inputs: ["name"],
    outputs: ["nameChange"],
    queries: {
        input: new ViewChild("ref")
    },
    host: {
        "(click)": "handleClick()",
        "[class.active]": "active"
    },
    template: `<input #ref />`
})
export class NgComponent extends defineComponent(setup) {}

function setup() {
    const state = reactive({
        name: "",
        nameChange: new EventEmitter(),
        input: undefined,
        handleClick() {},
        active: false
    })
    return state
}

Default values are then configured by the defineComponent factory function. The component type is inferred from its return value.

  • Usage of this

this is not available inside defineComponent() so we avoid circular references.

defineComponent(() => {
    function onClick() {
        this // not the `this` you'd expect!
    }
})

defineInjectable

Similar to defineComponent, this can be used to create injectable services using a factory function instead of a class.

import { HttpClient } from "@angular/common/http"
import { defineInjectable } from "ng-effects"

@Injectable({ providedIn: "root" })
export class NgService extends defineInjectable(ngService) {}

function ngService() {
    const http = inject(HttpClient)

    return {
        load(url) {
            return http.get(url)
        }
    }
}

If the service doesn’t need to be tree shakable, the service definition can be simplified.

const NgService = defineInjectable(() => {
    const http = inject(HttpClient)

    return {
        load(url) {
            return http.get(url)
        }
    }
})

@NgModule({
    providers: [NgService]
})
export class AppModule {}
  • Lifecycle hooks

The only lifecycle hook supported in Angular providers is ngOnDestroy. Similarly, when creating services with defineInjectable we can access the onDestroy hook to register cleanup functions.

const NgService = defineInjectable(() => {
    onDestroy(() => {
        // perform service cleanup
    })
})
  • Side Effect Invalidation

It’s possible to use both watch and watchEffect within services. As services are not bound to a particular view, effects will always be flushed synchronously regardless of the options passed.

const NgService = defineInjectable(() => {
    const count = ref(0)
    const http = inject(HttpClient)

    // will always be flushed synchronously, even if we set `flush`
    watchEffect((onInvalidate) => {
        const sub = http.post("url", { count: unref(count) })
        onInvalidate(() => sub.unsubscribe())
    })

    return {
        count
    }
})

Reactivity APIs

reactive

Takes an object and returns a reactive proxy of the original.

const obj = reactive({ count: 0 })

The reactive conversion is "deep": it affects all nested properties. In the ES2015 Proxy based implementation, the returned proxy is not equal to the original object. It is recommended to work exclusively with the reactive proxy and avoid relying on the original object.

  • Typing

declare function reactive<T extends object>(value: T): T

readonly

Takes an object (reactive or plain) or a ref and returns a readonly proxy to the original. A readonly proxy is deep: any nested property accessed will be readonly as well.

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // works for reactivity tracking
  console.log(copy.count)
})

// mutating original will trigger watchers relying on the copy
original.count++

// mutating the copy will fail and result in a warning
copy.count++ // warning!

ref

Takes an inner value and returns a reactive and mutable ref object. The ref object has a single property .value that points to the inner value.

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

If an object is assigned as a ref’s value, the object is made deeply reactive by the reactive method.

  • Access in Templates

When a ref is returned as a property on the render context (the object returned from defineComponent()) and accessed in the template, it automatically unwraps to the inner value. There is no need to append .value in the template:

<div>{{ count }}</div>
@Component()
export class NgComponent extends defineComponent(setup) {}

function setup() {
    return {
        count: ref(0)
    }
}
  • Access in Reactive Objects

When a ref is accessed or mutated as a property of a reactive object, it automatically unwraps to the inner value so it behaves like a normal property:

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

Note that if a new ref is assigned to a property linked to an existing ref, it will replace the old ref:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1

Note that ref unwrapping only happens when nested inside a reactive Object. There is no unwrapping performed when the ref is accessed from an Array or a native collection type like Map:

const arr = reactive([ref(0)])
// need .value here
console.log(arr[0].value)

const map = reactive(new Map([["foo", ref(0)]]))
// need .value here
console.log(map.get("foo").value)
  • Typing

interface Ref<T> {
    value: T
}

declare function ref<T>(value: T): Ref<T>

Sometimes we may need to specify complex types for a ref’s inner value. We can do that succinctly by passing a generics argument when calling ref to override the default inference:

const foo = ref<string | number>("foo") // foo's type: Ref<string | number>

foo.value = 123 // ok!

computed

Takes a getter function and returns an immutable reactive ref object for the returned value from the getter.

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // error

Alternatively, it can take an object with get and set functions to create a writable ref object.

const count = ref(1)
const plusOne = computed({
    get: () => count.value + 1,
    set: val => {
        count.value = val - 1
    }
})

plusOne.value = 1
console.log(count.value) // 0
  • Typing

// read-only
declare function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>

// writable
declare function computed<T>(options: {
  get: () => T
  set: (value: T) => void
}): Ref<T>

watchEffect

Run a function immediately while reactively tracking its dependencies, and re-run it whenever the dependencies have changed.

watchEffect(() => console.log(this.count))
// -> logs 0

setTimeout(() => {
    object.value++
    // -> logs 1
}, 100)

Stopping the Watcher

When watchEffect is called during a component’s defineComponent() function or lifecycle hooks, the watcher is linked to the component’s lifecycle, and will be automatically stopped when the component is destroyed.

It also returns a stop handle which can be called to explicitly stop the watcher:

const stop = watchEffect(() => {
    /* ... */
})

// later
stop()

Side Effect Invalidation

Sometimes the watched effect function will perform async side effects that need to be cleaned up when it is invalidated (i.e state changed before the effects can be completed). The effect function receives an onInvalidate function that can be used to register a invalidation callback. The invalidation callback is called when:

  • the effect is about to re-run

  • the watcher is stopped (i.e. when the component is destroyed if watchEffect is used inside defineComponent(), or lifecycle hooks)

watchEffect(onInvalidate => {
    const token = performAsyncOperation(id.value)
    onInvalidate(() => {
        // id has changed or watcher is stopped.
        // invalidate previously pending async operation
        token.cancel()
    })
})

We are registering the invalidation callback via a passed-in function instead of returning it from the callback.

watchEffect(async () => {
    data.value = await fetchData(this.id)
})

An async function implicitly returns a Promise, but the cleanup function needs to be registered immediately before the Promise resolves.

  • Effect Flush Timing

Angular Effects buffers invalidated effects and flushes them asynchronously to avoid unnecessary duplicate invocation when there are many state mutations happening in the same "tick". When a user effect is queued, it is always invoked after all component update effects:

<div>{{ count }}</div>
@Component()
export class NgComponent extends defineComponent(setup) {}

function setup() {
    const count = ref(0)

    watchEffect(() => {
        console.log(count.value)
    })

    return {
        count
    }
}

In this example:

  • The count will be logged synchronously on initial run.

  • When count is mutated, the callback will be called after the component has updated.

Note the first run is executed before the component view is initialized. So if you wish to access the DOM (or template refs) in a watched effect, do it in the onViewInit hook:

onViewInit(() => {
    watchEffect(() => {
        // access the DOM or template refs
    })
})

In cases where a watcher effect needs to be re-run synchronously or before component updates, we can pass an additional options object with the flush option (default is "post", executes during ngAfterViewChecked):

// fire synchronously
watchEffect(
    () => {
        /* ... */
    },
    {
        flush: "sync"
    }
)

// fire before component updates (executes during `ngDoCheck`)
watchEffect(
    () => {
        /* ... */
    },
    {
        flush: "pre"
    }
)
  • Typing

declare function watchEffect(
  effect: (onInvalidate: OnInvalidate) => void,
  options?: WatchEffectOptions
): StopHandle

interface WatchEffectOptions {
  flush?: "pre" | "post" | "sync",
  immediate?: boolean
}
type OnInvalidate = (invalidate: () => void) => void

type StopHandle = () => void

watch

watch requires watching a specific data source, and applies side effects in a separate callback function. It is also lazy by default - i.e. the callback is only called when the watched source has changed.

  • Compared to watchEffect, watch allows us to:

    • Perform the side effect lazily;

    • Be more specific about what state should trigger the watcher to re-run;

    • Access both the previous and current value of the watched state.

  • Watching a Single Source

A watcher data source can either be a getter function that returns a value, or directly a ref:

// watching a getter
const state = reactive({ count: 0 })
watch(
    () => state.count,
    (count, prevCount) => {
        /* ... */
    }
)

// directly watching a ref
const count = ref(0)
watch(count, (count, prevCount) => {
    /* ... */
})
  • Watching Multiple Sources

A watcher can also watch multiple sources at the same time using an Array:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})
  • Shared Behavior with watchEffect

watch shares behavior with watchEffect in terms of manual stoppage, side effect invalidation (with onInvalidate passed to the callback as the 3rd argument instead) and flush timing.

  • Typing

// watching single source
declare function watch<T>(
    source: WatcherSource<T>,
    callback: (
        value: T,
        oldValue: T,
        onInvalidate: OnInvalidate
    ) => void,
    options?: WatchOptions
): StopHandle

// watching multiple sources
declare function watch<T extends WatcherSource<unknown>[]>(
    sources: T,
    callback: (
        values: MapSources<T>,
        oldValues: MapSources<T>,
        onInvalidate: OnInvalidate
    ) => void,
    options?: WatchOptions
): StopHandle

//// inherits delay(1000) //// only triggers increment //// only triggers incrementAfterDelay //// only triggers squareAfterDelay

Lifecycle Hooks

Lifecycle hooks can be registered with directly imported onXXX functions:

import { onChanges, onViewInit, onDestroy } from "ng-effects"

export class NgComponent extends defineComponent(setup) {}

function setup() {
    onChanges(() => {
        console.log("ngOnChanges!")
    })
    onViewInit(() => {
        console.log("ngAfterViewInit!")
    })
    onDestroy(() => {
        console.log("onDestroy!")
    })
}

These lifecycle hook registration functions can only be used synchronously inside defineComponent, since they rely on internal global state to locate the current active instance (the component instance being called right now). Calling them without a current active instance will result in an error.

The component instance context is also set during the synchronous execution of lifecycle hooks, so watchers and computed properties created inside synchronously inside lifecycle hooks are also automatically torn down when the component is destroyed.

  • Mapping between Angular Lifecycle Hooks and Angular Effects

    • ngOnChanges → onChanges

    • ngOnInit → onInit

    • ngDoCheck → onCheck

    • ngAfterContentInit → onContentInit

    • ngAfterContentChecked → onContentChecked

    • ngAfterViewInit → onViewInit

    • ngAfterViewChecked → onViewChecked

    • ngOnDestroy → onDestroy

Dependency Injection (experimental)

inject enables dependency injection inside components without using reflection. It relies on unstable APIs that could change in future versions of Angular, so this feature is experimental.

When used in a component, inject retrieves values by walking the ElementInjector tree. This gives you access to special tokens such as ElementRef, Renderer2 and other component-specific tokens.

When used in an injectable service, inject retrieves values by walking the ModuleInjector tree instead. Angular Effects ensures the correct injector scope is used no matter where the value is provided.

For more information on the differences between these two modes, see hierarchical dependency injection in Angular.
import { Component } from "@angular/core"
import { defineComponent, inject } from "ng-effects"

@Component()
export class Descendant extends defineComponent(setup) {}

function setup() {
    const theme = inject(Theme, InjectFlags.SkipSelf | InjectFlags.Optional) ?? "light"
    return {
        theme
    }
}

inject accepts optional InjectFlags as a second argument. These are used to control dependency resolution or allow providers to be optional. When used with InjectFlags.Optional, default values can be passed using the nullish coalescing operator.

  • Injection Reactivity

To retain reactivity between provided and injected values, a ref can be used:

@Component({
    providers: [{
        provide: Theme,
        useValue: ref("dark")
    }]
})
export class Ancestor {}
  • Typing

declare function inject<T>(
    token: Type<T> | AbstractType<T> | InjectionToken<T>,
    flags: InjectFlags,
): T | null
declare function inject<T>(
    token: Type<T> | AbstractType<T> | InjectionToken<T>,
): T

declare enum InjectFlags {
    Default = 0,
    Host = 1,
    Self = 2,
    SkipSelf = 4,
    Optional = 8
}

Template Refs

Angular has several options for querying the template or content children of a component. If a component’s metadata contains queries, Angular attaches the query result to the component instance during the OnInit, AfterContentInit or AfterViewInit lifecycle hooks. In order to obtain a reference to an in-template element or component instance, we can declare a ref as usual and return it from defineComponent():

@Component({
    queries: {
        staticRef: new ViewChild("ref", { static: true }),
        dynamicRef: new ViewChildren("ref")
    },
    template: `
        <div #ref></div>
    `
})
export class NgComponent extends defineComponent(setup) {}

function setup() {
    const staticRef = ref<HTMLElement>()
    const dynamicRef = ref(new QueryList<HTMLElement>())

    watchEffect(() => {
        console.log(staticRef.value)
    })

    watchEffect(() => {
        for (const div of dynamicRef.value) {
            console.log(div)
        }
    })

    return {
        staticRef,
        dynamicRef
    }
}

Refs used as template refs behave just like any other refs: they are reactive and can be passed into (or returned from) composition functions.

Reactivity Utilities

unref

Returns the inner value if the argument is a ref, otherwise return the argument itself. This is a sugar function for val = isRef(val) ? val.value : val.

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x) // unwrapped is guaranteed to be number now
}

toRef

toRef can be used to create a ref for a property on a source reactive object. The ref can then be passed around and retains the reactive connection to its source property.

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRef is useful when you want to pass the ref of a prop to a composition function:

function setup() {
    useSomeFeature(toRef(props, 'foo'))
}

toRefs

Convert a reactive object to a plain object, where each property on the resulting object is a ref pointing to the corresponding property in the original object.

const state = reactive({
    foo: 1,
    bar: 2
})

const stateAsRefs = toRefs(state)
/*
Type of stateAsRefs:

{
    foo: Ref<number>,
    bar: Ref<number>
}
*/

// The ref and the original property is "linked"
state.foo++
console.log(stateAsRefs.foo) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

toRefs is useful when returning a reactive object from a composition function so that the consuming component can destructure / spread the returned object without losing reactivity:

function useFeatureX() {
    const state = reactive({
        foo: 1,
        bar: 2
    })

    // logic operating on state

    // convert to refs when returning
    return toRefs(state)
}

function setup() {
    // can destructure without losing reactivity
    const { foo, bar } = useFeatureX()

    return {
        foo,
        bar
    }
}

isRef

Check if a value is a ref object.

isProxy

Check if an object is a proxy created by reactive or readonly.

isReactive

Check if an object is a reactive proxy created by reactive.

It also returns true if the proxy is created by readonly, but is wrapping another proxy created by reactive.

isReadonly

Check if an object is a readonly proxy created by readonly.

Advanced Reactivity APIs

customRef

Create a customized ref with explicit control over its dependency tracking and update triggering. It expects a factory function. The factory function receives track and trigger functions as arguments and should return an object with get and set.

Example using a custom ref to implement debounce:

function useDebouncedRef(value, delay = 200) {
    let timeout
    return customRef((track, trigger) => {
        return {
            get() {
                track()
                return value
            },
            set(newValue) {
                clearTimeout(timeout)
                timeout = setTimeout(() => {
                    value = newValue
                    trigger()
                }, delay)
            },
        }
    })
}

function setup() {
    return {
        text: useDebouncedRef("hello")
    }
}
  • Typing

declare function customRef<T>(factory: CustomRefFactory<T>): Ref<T>

type CustomRefFactory<T> = (
    track: () => void,
    trigger: () => void,
) => {
    get: () => T
    set: (value: T) => void
}

markRaw

Mark an object so that it will never be converted to a proxy. Returns the object itself.

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// also works when nested inside other reactive objects
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

markRaw and the shallowXXX APIs below allow you to selectively opt-out of the default deep reactive / readonly conversion and embed raw, non-proxied objects in your state graph. They can be used for various reasons:

Some values simply should not be made reactive, for example a complex 3rd party class instance, or an Angular component object.

Skipping proxy conversion can provide performance improvements when rendering large lists with immutable data sources.

They are considered advanced because the raw opt-out is only at the root level, so if you set a nested, non-marked raw object into a reactive object and then access it again, you get the proxied version back. This can lead to identity hazards - i.e. performing an operation that relies on object identity but using both the raw and the proxied version of the same object:

const foo = markRaw({
  nested: {}
})

const bar = reactive({
  // although `foo` is marked as raw, foo.nested is not.
  nested: foo.nested
})

console.log(foo.nested === bar.nested) // false

Identity hazards are in general rare. But to properly utilize these APIs while safely avoiding identity hazards requires a solid understanding of how the reactivity system works.

shallowReactive

Create a reactive proxy that tracks reactivity of its own properties, but does not perform deep reactive conversion of nested objects (exposes raw values).

const state = shallowReactive({
    foo: 1,
    nested: {
        bar: 2
    }
})

// mutating state's own properties is reactive
state.foo++
// ...but does not convert nested objects
isProxy(state.nested) // false
state.nested.bar++ // non-reactive

shallowReadonly

Create a proxy that makes its own properties readonly, but does not perform deep readonly conversion of nested objects (exposes raw values).

shallowRef

Create a ref that tracks its own .value mutation but doesn’t make its value reactive.

const foo = shallowRef({})
// mutating the ref's value is reactive
foo.value = {}
// but the value will not be converted.
isReactive(foo.value) // false

toRaw

Return the raw, original object of a reactive proxy. This is an escape hatch that can be used to temporarily read without incurring proxy access / tracking overhead or write without triggering changes. It is not recommended to hold a persistent reference to the original object. Use with caution.

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

Prior Arts

This library and its documentation are based on Vue Composition API