Higher order composition layer #2084
Replies: 2 comments
-
We think patterns like these tend to re-implement classes without the standard class syntax. Rather than having developers have to learn our particular declarative object setup API, we'd rather use familiar classes. Translated to the LitElement pattern, I think the example is much more declarative and readable. You can see what state is associated with the element by scanning the class fields. There's no large difference in how reactive properties and reactive state are declared. Event handlers are just methods: class XModalEditor extends LitElement {
static styles = css`
.dialog {
background-color: #fff;
margin: auto;
width: 50vw;
display: flex;
flex-direction: column;
}
`;
@property({ type: Boolean }) open;
@property() name;
@state() private _timesChanged = 0;
private _onNameChanged(e: Event) {
this.timesChanged += 1;
this.dispatchEvent(new CustomEvent('name-changed', { detail: { name: e.target.value } }));
}
private _onCloseClicked() {
this.dispatchEvent(new CustomEvent('open-changed', { detail: { open: false } }));
};
render() {
if (!(this.open ?? false)) return;
return html`
<div class="dialog">
<div class="container">
<input
type="text"
.value=${live(this.name ?? '')}
@input=${this._onNameChanged} />
<div>
This is the ${this.timesChanged} time that you edit the name
</div>
</div>
<div class="bottom-bar">
<button
@click=${this._onCloseClicked}>
<span>Close</span>
</button>
</div>
</div>
`;
};
} As for your points:
Decorators are very statically analyzable - that's the main reason we use them. TypeScript and other tools look at them as just class fields, which is what we want - and to developers they are just a declarative way to add extra behavior.
Class can be built up by composition too. This is why we're adding constructs like reactive controllers and making it easier for directive to hook the host lifecycle. I'm not sure about the easier to reason about claim. It's subjective, but we think that staying close to familiar class syntax will be the easiest for JS devs to reason about over time. They won't have to understand our particular semantics.
I don't think the way the component is declared affects this. We encourage that with classes too.
I don't know that creating closures as statements and expressions is any more liberating than class methods, but it's certainly less declarative. Private class fields are good in my option. They clearly indicate which state the object has in the same location and syntax as the public state. Standard private fields and decorators will make this even better - that's what we're aiming for. |
Beta Was this translation helpful? Give feedback.
-
They don't seem very statically analyzable to me, here are some examples. The following line shows the signature of property decorators, the value of the property cannot be constrained to a certain type. For instance, declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; Another example, let's say we wanted to create a function computed <K extends string>(
dependencies: readonly K[]
): (obj: { [key in K]: unknown }, prop: string | symbol, descriptor: PropertyDescriptor) => void When this decorator is applied to a class, typescript will throw an error. class Foo () extends LitElement {
@property()
foo: string;
@internalProperty()
protected bar: string;
@computed(['foo', 'bar'] as const)
get bbb () {
return `${foo}-${bar}`;
}
} Decorators cannot infer the type of protected properties. [tsserver 2345] [E] Argument of type 'bar' is not assignable to parameter of type '{ foo: unknown; bar: unknown; }'.
Property 'bar' is protected in type 'Foo' but public in type '{ foo: unknown; bar: unknown; }'. Compare that decorator with the function const shouldRender = useComputed(host, ([disabledValue, disablingValue]) => {
return !(disabledValue ?? false) || disablingValue;
}, [disabled, disabling] as const); Typescript can verify that the function above is type-safe and has the following signature, it doesn't matter if properties are internal or external. useComputed<boolean, readonly [ReactiveGetter<boolean | undefined>, ReactiveGetter<boolean>], ComponentInstance<Props>>(
host: ComponentInstance<Props>,
fn: (args: [boolean | undefined, boolean]) => boolean,
observedDeps: readonly [ReactiveGetter<boolean | undefined>, ReactiveGetter<boolean>],
{ hasChanged }?: UseComputedOptions<...> | undefined
): ReactiveGetter<boolean>
I've seen many developers make the following mistake when creating a component. private _onNameChanged(e: Event) {
this.timesChanged += 1;
// this.dispatchEvent(new CustomEvent('name-changed', { detail: { name: e.target.value } }));
this.name = e.target.value;
} And since they don't know about reactive patterns, they invent something similar to a polling pattern, asking children about their values. I know that LitElement encourages reactive patterns. What I propose is to encourage them even more, by establishing a clearer boundary between external and internal properties and making external properties strictly readonly. EDIT: changed the example for non-typesafe decorators. |
Beta Was this translation helpful? Give feedback.
-
I propose to make components easier to build and maintain by creating a higher order composition layer on top of LitElement 3.
This is how components would look like.
It's based on the following principles.
Here is a working demo. All the implementations details and examples can be found in this repository.
The result is something similar to hooks but simpler, it doesn't need magic or compilation.
There are some rough edges, notably that properties (external and internal) are getters, not values. This is similar to SolidJS, which uses a proxy for properties (acting as a getter) and regular getters for internal properties.
Beta Was this translation helpful? Give feedback.
All reactions