RFC: Angular Composition API #11
Replies: 20 comments 11 replies
-
Let me start off by saying that I have much respect for the effort being put in here. However, after working with React hooks for quite a some time, I came to really dislike the whole "useWhatever" hooks approach for state management. Perhaps this is not the most objective feedback, but rather my initial feelings. |
Beta Was this translation helpful? Give feedback.
-
@DmitryEfimenko Thank you for your feedback. To clarify, the proposed API is designed to work in tandem with RxJS specifically. On the surface you can consider the following two snippets to be conceptually equivalent. Plain RxJS const value = new BehaviorSubject(0)
value.subscribe((current) => {
console.log(current)
}) Composition API const value = use(0) // uses BehaviorSubject under the hood, returns an interop observable. Interchangeable.
subscribe(value, (current) => {
console.log(current)
}) The problems that
There are some use cases where it might not be appropriate to use the const value = new BehaviourSubject(0)
const sink = subscribe()
sink.add(value.subscribe((current) => {
console.log(current)
})) To your last point about data flow control, |
Beta Was this translation helpful? Give feedback.
-
I agree with @DmitryEfimenko If I jump into the role of a PO my biggest question is what do you gain from this? The proposed code is just equivalent to Vue 3's composition API which doesn't provide any benefit for me as a developer in Angular or at least you didn't sell it to me? Is it more performant? Is it reducing the bundle size? Does it make testing simpler? Does it reduce complexity? To extend on that vue 3 introduced the composition API to manage code reuse patterns, typescript support and readability or large components. TypeScript is not an issue in Angular as it is a first class citizen. Lastly: I really like to look beyond the tellerand and see what other libraries and frameworks do. But not everything needs to be copied, as it is often a result of a problem that occured. And one should always as themself do we have that problem or do we have a different approach to solve that problem already in place? |
Beta Was this translation helpful? Give feedback.
-
@chaosmonster Good questions, I will try to answer briefly.
|
Beta Was this translation helpful? Give feedback.
-
I've added a Sierpinski triangle bench demo. Refer to this issue for perf comparisons. View demo here |
Beta Was this translation helpful? Give feedback.
-
Added a import { Component } from "@angular/core"
import { listen, ViewDef } from "@mmuscat/angular-composition-api"
function setup() {
listen<MouseEvent>("click", (event) => {
console.log("clicked!", event)
})
listen("window:scroll", () => {
console.log("scrolling!")
})
return {}
}
@Component()
export class MyComponent extends ViewDef(setup) {} |
Beta Was this translation helpful? Give feedback.
-
I've added a composition equivalent to the function setup() {
const enabled = attribute("enabled", Boolean)
const results = use()
const loadApi = inject(API)
subscribe(loadApi().pipe(
filter(enabled)
), { next: results })
return {
results
}
}
@Component({
selector: "my-component"
})
export class MyComponent extends ViewDef(setup) {} <my-component enabled></my-component> |
Beta Was this translation helpful? Give feedback.
-
Adding a const count = use(0)
const disabled = use(false)
const state = combine({
disabled,
nested: {
count
},
name: "Plain Jane"
})
state() // { disabled: true, nested: { count: 0 }, name: "Plain Jane" }
state({
disabled: true,
nested: {
count: 10
},
name: "Still Plain Jane"
})
count() // 10
disabled() // true
count(20) // updates `state`
state().count // 20 |
Beta Was this translation helpful? Give feedback.
-
Two more convenience methods for working with
const count = use(0)
const disabled = use(false)
subscribe(() => {
const state = get({ count, disabled }) // reactive
})
const count = use(0)
const disabled = use(false)
subscribe(() => {
const state = access({ count, disabled }) // not reactive
}) |
Beta Was this translation helpful? Give feedback.
-
I think the new additions are great. I can see how they make this composition API even more powerfull than similar APIs from other frameworks due to their ability to also reactively fetch information from the DOM. One comment though to the naming of What do you think about the name |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
Adding a The basic signature is: declare function pipe<T, A>(observableInput: Observable<T>, op1: OperatorFunction<T, A>): Value<A | undefined> function setup() {
const userId = use(EMPTY)
const loadTodos = inject(LoadTodos)
const todos = pipe(userId, exhaustMap(loadTodos))
// can set
todos([])
todos.next([])
// can read
todos()
todos.value
// can subscribe
subscribe(todos, {
next() {},
error() {},
})
return {
userId,
todos, // unwrapped in view
}
} The pipe expression const todos = pipe(userId, exhaustMap(loadTodos)) Is equivalent to const todos = use(userId.pipe(exhaustMap(loadTodos))) Extraction function loadTodos(userId) {
return pipe(userId, exhaustMap(inject(LoadTodos)))
}
function setup() {
const userId = use(EMPTY)
const todos = loadTodos(userId)
return {
userId,
todos,
}
} Overloads // if first argument is not an observable input, behave like rxjs pipe
declare function pipe<T, A>(op1: OperatorFunction<T, A>): OperatorFunction<T, A>
declare function pipe<T, A, B, C, D, E, F, G, H, I>(observableInput: Observable<T>, op1: OperatorFunction<T, A>, op2...op8, op9: OperatorFunction<H, I>): DeferredValue<I>
declare function pipe<T, A, B, C, D, E, F, G, H, I>(op1: OperatorFunction<T, A>, op2...op8, op9: OperatorFunction<H, I>): OperatorFunction<I> |
Beta Was this translation helpful? Give feedback.
-
I want to improve the composition API error handling story. It's never been easy to handle errors with rxjs in a way that lets you:
Attempting to do this with operators is messy and tightly couples error handling and retry logic with observables that are primarily concerned with transforming data. Proposal: Add error hooks to function loadTodos(userId) {
return pipe(userId, exhaustMap(inject(LoadTodos)))
}
function setup() {
const userId = use(EMPTY)
const todos = loadTodos(userId)
const reload = use(Function)
const error = onError(todos, (error) => {
console.error(error)
return reload
})
return {
userId,
error,
todos,
reload
}
}
@Component({
selector: "todos",
template: `
<div *ngIf="error else showTodos">
Something went wrong.
<button (click)="reload()">Reload</button>
</div>
<ng-template #showTodos>
<spinner *ngIf="!todos"></spinner>
<div *ngFor="let todo of todos">
<todo [value]="todo"></todo>
</div>
</ng-template>
`
})
export class Todos extends ViewDef(setup) { } The interface ErrorState {
retries: number // number of times this value has been retried
message?: string // error message if provided
error: unknown // original error object
} By intercepting errors with
function setup() {
const anyError = onError((error) => {
// listen to any error
if (error instanceof HttpError) {
console.error(error)
}
// rethrow to dispatch to ErrorHandler service
throw error
})
return {
anyError
}
} Other considerations:
|
Beta Was this translation helpful? Give feedback.
-
Docs added for 0.1304.1 release |
Beta Was this translation helpful? Give feedback.
-
Exploring composition of dynamic components with automatic handling of input and output bindings. Probably not going to add this to the core lib. @Component()
class Widget {
@Input() count
@Output() countChange
}
function setup() {
const count = use()
const countChange = use(Function)
const component = use(Widget)
const widget = render(component, {
count,
countChange
})
return {
widget
}
}
@Component({
template: `
<ng-container *ngRender="widget"></ng-container>
`
})
export class MyComponent extends ViewDef(setup) {}
|
Beta Was this translation helpful? Give feedback.
-
Generalising the dynamic component example above, a JSX syntax using subjects could be possible. Note this is purely syntactic sugar, there is no VDOM or any react nonsense here. This doesn't allow for directives either. function setup() {
const count = use()
const countChange = use(Function)
const widget = <ParentComponent
count={count}
countChange={countChange}
>
<ChildComponent count={count}>
<div>hello {count}</div>
</ChildComponent>
</ParentComponent>
return {
widget,
count,
countChange
}
} Would be equivalent to function setup() {
const count = use()
const countChange = use(Function)
const widget = [ParentComponent, {
count,
countChange
}, [
[ChildComponent, {
count
}, [
["div", null, [`hello ${count}`]],
]]
]]
return {
widget,
count,
countChange
}
} Then fed into |
Beta Was this translation helpful? Give feedback.
-
This RFC looks great. And the utilities you presented look promising as well. How can we create some more traction? Any plans on gathering more feedback? Maybe we can post this discussion in the Angular issue board? Or some other platform where we can reach more community members? |
Beta Was this translation helpful? Give feedback.
-
Reworking store API, will expand later: https://github.com/mmuscat/angular-composition-api/tree/master/packages/store |
Beta Was this translation helpful? Give feedback.
-
Adding an
Example function setup() {
const count = use(0)
const countChange = listen(count)
const changes = onChanges(count, (change) => {
console.log(change.first) // false
console.log(change.current)
console.log(change.previous)
})
console.log(changes.first) // true
console.log(change.current) // 0
console.log(change.previous) // undefined
countChange(10) // does NOT trigger `onChanges`
return {
count,
countChange
}
}
@Component({
template: `
<!-- Template assignment triggers `onChanges` -->
<counter [value]="count" (valueChange)="count = $event"></counter>
`,
// inputs trigger `onChanges`
inputs: ["count"]
})
export class MyComponent extends ViewDef(setup) {}
|
Beta Was this translation helpful? Give feedback.
-
0.1306.0 released, some minor interface changes and first release of Phalanx state store. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
RFC: Angular Composition API
This topic is an open discussion for the current and future development of Angular Composition API. If you have an issue please open a separate thread.
Why Angular needs a composition API
State
Angular is an opinionated framework, but leaves open the question of how state should be managed in our application. Out of the box we are presented with a mix of imperative and reactive styles for state management, which is a barrier to entry for purely reactive state.
A composition API solves this by filling in the gaps in Angular's reactive model, providing a consistent pattern for reactive state management.
Fig 1a. Imperative style
Fig 1b. Reactive composition
These two examples might look similar, but the latter example has a few advantages already:
We can observe changes to the value of
count
, even it's an input or not.We can extract the logic and side effect into another function, which is not possible with the first example.
Fig 1c. Extraction
Subscriptions
Subscriptions are another pain point that Angular leaves us to figure out for ourselves. Current approaches in the ecosystem include:
Out of the box Angular gives us a pipe that automatically handles subscriptions to observable template bindings.
Fig 2. Async pipe binding
The benefits of this approach is that we do not have to worry about the timing of the subscription, since it will always happen when the view is mounted, and the view will be updated automatically when values change.
However in real world applications it is easy to accidentally over-subscribe to a value because you forgot to
share()
it first. Templates with many temporal async bindings are much harder to reason about than static templates with synchronous state.Another popular approach is to subscribe to observables in our component class, using a sink to simplify subscription disposal.
Fig 3. Subscription sink with imperative subscribe
Sinks are a good way to deal with imperative subscriptions, but results in more verbose code. Other approaches use
takeUntil
, but that has its own pitfalls. The only guaranteed way to dispose of a subscription is to call itsunsubscribe
method.The downside to this approach is we have to manually handle change detection if using the
OnPush
change detection strategy. The timing of the subscription here also matters, causing more confusion.Let's see how composition solves these problems.
Fig 4. Composable subscriptions with reactive state
The composition API runs in an Execution Context with the following behaviour:
Subscriptions are deferred until the view has mounted, after all inputs and queries have been populated.
Change detection runs automatically whenever a value is emitted, after calling the observer. State changes are batched to prevent uneccessary re-renders.
Subscriptions are automatically cleaned up when the view is destroyed.
Reactive values are unwrapped in the component template for easy, synchronous access.
Lifecycle
The imperative style of Angular's lifecycle hooks work against us when we want truly reactive, composable components.
Fig 5. A riddle, wrapped in a mystery, inside an enigma
The composition API provides a Layer of Abstraction so we don't have to think about it.
Fig 6. Composition API lifecycle
Fine tune control is also possible using the
Context
scheduler.Fig 7. Before/After DOM update hooks
Change Detection
Angular's default change detection strategy is amazing for beginners in that it "just works", but not long after it becomes necessary to optimise performance by using the
OnPush
strategy. However in this change detection mode you must manually trigger change detection after an async operation by callingdetectChanges
somewhere in your code, or implicitly with theasync
pipe.By comparison, the composition API schedules change detection automatically:
ViewDef
emitsFig 8. Composition API change detection
Changes to reactive state are batched so that the view is only checked once when multiple values are updated in the same "tick".
How composition works
TBD
Execution context
TBD
Reactive blocks
TBD
Functional groups
TBD
Change detection
TBD
Angular Composition API
This RFC includes a reference implementation. Install it with one of the commands below.
Built for Ivy
Angular Composition API wouldn't be possible without the underlying changes brought by the Ivy rendering engine.
Built for RxJS
Other libraries achieve reactivity by introducing their own reactive primitives. Angular Composition API builds on top of the existing RxJS library. The result is a small api surface area and bundle size. You already know how to use it.
Built for the future
There is currently talk of adding a view composition API to a future version of Angular. It is hoped that this library can provide inspiration for that discussion and potentially integrate with any new features that might bring.
Prior Arts
React Hooks
Vue Composition API
Angular Effects
Beta Was this translation helpful? Give feedback.
All reactions