I have decided to release this version as it is so that future work can be directed towards the upcoming release of Angular 10. The upstream library this is based on (@vue/reactivity
) is still in BETA and a number of changes have been made to make it compatible with Angular, some of which depend on undocumented private APIs. If you spot a mistake or get stuck, please open an issue.
This minor release introduces a Composition API similar to @vue/reactivity
, with special considerations for Angular and RxJS interop in particular. Key features like effect invalidation, lifecycle hooks and reactivity have been replicated in terms of the API, with some minor changes to make it fit with how Angular’s component lifecycle and change detection process works.
This is still a work in progress!
Note
|
The 9.0.x decorator API has been deprecated and will be removed in 10.0.0. |
Example illustrating some core APIs:
import { computed, defineComponent, defineInjectable, inject, observe, ref } from "ng-effects"
@Injectable({ providedIn: "root" })
export class ApiService extends defineInjectable(() => {
const http = inject(HttpClient)
function loadData(id: number) {
return http.get<{ name: string }[]>(`http://www.example.com/api/entity/${id}`)
}
return {
loadData
}
}) {}
@Component({
selector: "app-root",
inputs: ["count"]
})
export class AppRoot extends defineComponent(() => {
const count = ref(0)
const countChange = new EventEmitter<number>()
const double = computed(() => count.value * 2)
const { loadData } = inject(ApiService)
const onLoadData = effect<number>().pipe(
exhaustMap(loadData)
)
function increment() {
count.value += 1
onLoadData.next(count.value)
}
watchEffect((onInvalidate) => {
console.log(count.value)
// effect invalidation callback
onInvalidate(() => countChange.next(count.value))
})
observe(onLoadData, (list) => {
for (item of list) {
console.log(item.name)
}
})
return {
count,
double,
increment
}
}) {}
-
use same
reactive
andreadonly
implementation as@vue/reactivity
-
add
effect
factory -
add
observe
hook -
add
observeError
hook -
add
fromRef
util
-
fix some incorrect types when unwrapping refs
-
don’t unwrap refs returned from services
-
ensure effects run synchronously when used outside of a view context
This release features a core rewrite that reduces the code footprint, fixes bugs and aligns more closely with the behaviour of Vue Composition API. Component classes now extend a base class produced by defineComponent
, which takes a factory function analagous to the setup
function from Vue. Directives and services receive similar treatment with defineDirective
and defineInjectable
. Refs are introduced, bringing the API coverage close to parity.
-
add
ref
,shallowRef
andcustomRef
-
remove
effect
andEffectFactory
-
rename
Computed
tocomputed
, remove$
alias, rework with refs -
add
readonly
andshallowReadonly
-
add
toRef
,toRefs
,isRef
,unref
utils -
remove
connectable
andConnectable
-
add
defineComponent
,defineDirective
anddefineInjectable
-
rework
inject
usingɵsetCurrentInjector
andɵɵdirectiveInject
-
rename and add missing lifecycle methods
-
launch api reference docs site (https://ngfx.io)
This releases reintroduces Effect
as a reactive type and adds support for computed properties with Computed
(alias: $
). It also adds options to control when watchEffect
is flushed.
Effect
works much the same as HostEmitter
in 9.0.0 except it now takes an optional OperatorFunction
argument. This makes it simple to delay or transform value emissions from the source. Users can bypass this operator by subscribing to new Effect(fn).source
.
Computed
properties are implemented as a function setter/getter. Calling the method without arguments will return the memoized value or recompute the value if reactive dependencies have changed. Optional arguments can also be passed in as part of the value computation. These arguments are also memoized. Only the most recent deps/arguments are cached.
@Component({
template: `
<button (click)="increment(1)">Click</button>
<div>Offset: {{ offset(1) }}</div>
`
})
export class NgComponent extends Connectable {
@Input()
count = 0
@Output()
countChange = new Effect<number>(delay(500))
increment = new Effect<number>()
offset = $((offset: number) => this.count + offset)
ngOnConnect() {
const { increment, countChange } = this
effect((onInvalidate) => {
const cleanup = increment.subscribe((num) => {
this.count += num
countChange(this.count)
})
onInvalidate(cleanup)
})
}
}
You can now configure watchEffect
to be flushed before the component view updates. The current timings are:
Option | Lifecycle |
---|---|
sync |
Whenever a reactive property changes. |
pre |
After all |
post (default) |
After all |
-
support
pre
andsync
options forwatchEffect
-
add new
Effect
andComputed
types -
rework
onInvalidate
as an injected argument to effects -
remove global
onInvalidate hook
-
reactive no longer wraps methods in an execution context
This release adds reactive hooks for Angular component lifecycle methods:
@Component()
export class MyComponent extends Connectable {
ngOnConnect() {
// called during ngOnInit
onChanges((changes) => {
// when inputs change
})
afterContentInit(() => {
// content children initialised
})
afterViewInit(() => {
// after first render
effect(() => {
// starts after component mounted
})
})
afterContentChecked(() => {
// after content children updated
})
afterViewChecked(() => {
// after each render
})
onDestroy(() => {
// when component destroyed
})
}
}
-
add more lifecycle hooks
-
rework
onChanges
hook so it only fires when inputs are changed -
return stop handler from effects
-
fix invalidations for effects inside lifecycle hooks
-
export
onInvalidate
hook -
ensure invalidations are only called once on destroy
This release adds side effect invalidation hooks. These hooks can be called inside the top level of an effect or connected component method to register side effect invalidations, such as cancelling a http call. There are two global hooks available: onInvalidate
and onDestroy
.
OnInvalidate
is called each time an effect or connected component method is invoked, as well as when the component is destroyed.
OnDestroy
is only called when the component is destroyed.
@Component()
export class MyComponent extends Connectable {
private http = inject(HttpClient)
count = 0
asyncMethod() {
const sub = this.http.get("/api/count").subscribe((count) => {
this.count = count
})
onDestroy(() => {
sub.unsubscribe()
})
}
ngOnConnect() {
const asyncLogger = inject(AsyncLogger)
watchEffect(() => {
const cancel = asyncLogger.logAfterDelay(this.count, 500)
onInvalidate(() => {
cancel() // called each time watchEffect deps change
})
})
}
}
-
allow onInvalidate and onDestroy in component methods
-
add side effect invalidation callbacks
-
fall back to global injector when
inject
is called outside of component context
This release introduces a composition/hooks model based on Vue 3’s Composition API. This will replace the decorator API, which has been deprecated.
We can now use functional composition with context-aware hooks to execute reactive effects.
const MyConnectable = connectable<AppComponent>((context) => { // connectable provider injected with reactive context
// inject(HttpClient) dependency injection allowed in setup
afterViewInit(() => { // lifecycle hooks
effect(() => {
// return teardown logic
// cleaned up when component destroyed or effect is invalidated
})
})
// available hooks:
// - OnChanges: fires every time a component property change is detected
// - AfterViewInit: fires once when component is first mounted
// - WhenRendered: fires every time the component view updated
// - OnDestroy: fires once when the component is being destroyed
})
@Component({
selector: "app-root",
template: `
<div>Count: {{ count }}</div>
`,
providers: [MyConnectable] // executed after ngOnConnect
})
export class AppComponent extends Connectable { // base class required
@Input()
count = 0 // state
private http = inject(HttpClient) // dependency injection allowed in initializers
incrementCount() { // method
// inject(HttpClient) dependency injection allowed in methods
this.count += 1
}
ngOnConnect() { // setup
// inject(HttpClient) dependency injection allowed in setup
effect(() => // basic effect, no tracking
interval(1000).subscribe(() => this.incrementCount()) // increment count once per second
)
watchEffect(() => { // reactive effect, dependency tracking
console.log(this.count) // logs count whenever it changes
})
}
}
-
use IterableDiffers for effect invalidation
-
add utils, add effect options, create untracked effect separate to watchEffect
-
allow
inject()
inside component methods -
allow
inject()
inside property initializers -
add
connectable
hook -
add
ngOnConnect
hook -
throw error when injecting outside of a valid injection context
-
add experimental composition api
-
fix reactive factory
-
fix change detection, dependency injection
-
fix circular deps, initial change detection, create test component
-
fix memory leak
-
tap ngDoCheck lifecycle hook in effects scheduler
-
update changelog
-
fix types for typescript 3.8
-
fix error when accessing reactive state outside injection context
-
deprecate decorator API
The decorator API will be removed and replaced by the composition API in 10.0.0.
-
Connect
-
HOST_INITIALIZER
-
Effect
-
State
-
Context
-
Observe
-
HostRef
-
EffectMetadata
-
EffectAdapter
-
CreateEffectAdapter
-
NextEffectAdapter
-
DefaultEffectOptions
-
BindEffectOptions
-
AssignEffectOptions
-
AdapterEffectOptions
-
EffectOptions
-
ObservableSources
-
CONNECT
-
effects
-
Effects
-
USE_EXPERIMENTAL_RENDER_API
-
changes
-
latest
-
ViewRenderer
The composition API relies on ES6 Proxy objects to create the proper execution context for connected components. This means dropping support for older browsers that don’t support them.
Effect adapters that implement the CreateEffectAdapter
interface now receive the whole effect function as an argument instead of the invoked return value. This means effect adapters can take full control of the effect and supply the effect function with arbitrary arguments, invoke the function multiple times, etc.
Before
@Injectable()
export class MyAdapter implements EffectAdapter<number> {
create(value: Observable<number>, metadata: EffectMetadata) {
return value.pipe(
delay(500)
)
}
next(value: number) {
console.log(value)
}
}
After
type EffectFn = (state: State<any>, customArg: string) => Observable<number>
@Injectable()
export class MyAdapter implements EffectAdapter<EffectFn> {
constructor(private hostRef: HostRef) {}
create(effectFn: EffectFn, metadata: EffectMetadata) {
return effectFn(this.hostRef.state, "CUSTOM_ARG")
}
next(value: number) {
console.log(value)
}
}
-
effects no longer need to be provided with
effects()
-
rework
effects()
as an optional provider to configure defaults -
remove
HOST_EFFECTS
provider -
add
Effects
provider as a replacement foreffects()
andHOST_EFFECTS
-
fix typed metadata in effect adapters
-
enforce return types when using effect adapters
-
workaround for
InjectFlags.Self
(#3) -
check if view destroyed before marking view dirty
effects()
is now only used to optionally configure default options. To run effects, provide the Effects
token along with any other effect providers. Host effects only need the Effects
token to run.
Before
@Component({
providers: [effects([MyEffects, ...etc]), MyAdapter] // or [HOST_EFFECTS]
})
export class AppComponent {
@Effect(MyAdapter)
hostEffect() {}
constructor(connect: Connect) {
connect(this)
}
}
After
@Component({
providers: [Effects, MyEffects, MyAdapter, ...etc] // or [Effects]
})
export class AppComponent {
@Effect(MyAdapter)
hostEffect() {}
constructor(connect: Connect) {
connect(this)
}
}
Only effects provided at the same level as the component or directive will be executed. Effects are not inherited from parent injectors and must be provided in every component that uses it.
-
export missing tokens and tweak defaults
The default value of markDirty
will now be true
if the effect configures a bind
or assign
option. This is a better default in most cases, and can be configured by setting @Effect("prop", { markDirty: false })
.
-
add experimental global
connect
function -
add host observer as third argument to effect methods
-
return cached metadata for already seen effect tokens
-
create effects in effect runner instead of explorer
-
create adapter in effect runner instead of explorer
-
make
markDirty
calls synchronous unless in noop zone -
reduce usage of rxjs operators
-
updated docs