Skip to content

[reactive-element] Adds skipping initial attribute reflection for default values.#4895

Closed
sorvell wants to merge 17 commits intolit:mainfrom
sorvell:default-value
Closed

[reactive-element] Adds skipping initial attribute reflection for default values.#4895
sorvell wants to merge 17 commits intolit:mainfrom
sorvell:default-value

Conversation

@sorvell
Copy link
Member

@sorvell sorvell commented Jan 17, 2025

Adds support for lit/rfcs#36 and helps facilitate shoelace-style/shoelace#2339.

Spawning an initial attribute can cause a NextJS hydration error, see repro.

To use, set defaultValue in property options, and avoid setting the default in the field/accessor.

@property({reflect: true, defaultValue: 'foo'})
variant: string;

Previously, this would spawn a variant="foo" attribute. With this PR and the above settings, it will not.

<my-element></my-element>

In addition, the default value will be set when the attribute is removed.

@changeset-bot
Copy link

changeset-bot bot commented Jan 17, 2025

🦋 Changeset detected

Latest commit: 3dfc1ce

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@lit/reactive-element Minor
lit Minor
lit-element Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@sorvell sorvell marked this pull request as ready for review January 19, 2025 00:34
@sorvell sorvell requested a review from kevinpschaaf as a code owner January 19, 2025 00:34
@sorvell sorvell requested review from justinfagnani and removed request for kevinpschaaf January 19, 2025 13:34
@sorvell sorvell mentioned this pull request Jan 20, 2025
@justinfagnani
Copy link
Collaborator

justinfagnani commented Jan 21, 2025

Nice!

My main comment before looking at code is that I don't think we should have the option skipReflectInitial. Attributes should ideally never reflect initially. If you give a default value, I see no benefit to reflecting it, even for styling.

If for some reason in the future enabling initial value reflection is important, we could add an reflectInitial option, but I wouldn't include it now.

I also think the option name should be defaultValue. value sounds too much like the value can't change. "default" implies better to me that that value will be used when the attribute is removed.

@sorvell sorvell requested a review from justinfagnani January 21, 2025 13:40
}
return v;
this._$changeProperty(name, undefined, options, true);
return v ?? (options.defaultValue as V);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about a dev mode warning that they have both a default value and an initializer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not really possible to detect for all cases, and having a warning that sometimes shows just seems confusing and hard to maintain.

For example, decorator v. not, accessor v. field, instance shadowing, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope in the future we can move to standard decorators only and this will be the place we warn. I guess not that many people would use it now. We can try to add a lint warning somewhere.

continue;
}
const value = this[p as keyof this];
// Ensure any initially set value is seen as a change.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to do this for default values? It kind of makes sense to me to consider the initial default value not changed.

Or maybe that's what this does? Might be clearer if the comments are inside the conditions? If so, maybe mention that in the jsdocs for the defaultValue option too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the comments clearer. Properties with defaultValue set are currently included in initial changes.

We could choose to change this but the number of different ways properties can be configured makes this really hard to manage. (In fact there are some issues with this currently: accessor defined properties are only handled specially with the decorator and not in JS.)

Steve Orvell and others added 4 commits February 3, 2025 16:21
@justinfagnani
Copy link
Collaborator

Awesome @sorvell!!

We need to update lit.dev for this feature. Can you do that?

@sorvell
Copy link
Member Author

sorvell commented Feb 4, 2025

Docs PR: lit/lit.dev#1396

@Lookwe69
Copy link

Lookwe69 commented Feb 4, 2025

defaultValue shouldn't be a function ? It can be eager when the default value is an object. With a function, we can delay the creation only when needed.

@justinfagnani justinfagnani changed the title Adds skipping initial attribute reflection for default values. [reactive-element] Adds skipping initial attribute reflection for default values. Feb 12, 2025
@sorvell sorvell marked this pull request as draft February 14, 2025 00:03
@sorvell
Copy link
Member Author

sorvell commented Feb 14, 2025

19963dd adds setting the default value when the attribute is removed.

It also changes the strategy used to ensure the default value is the value of the property at construction time. We need to modify the getter to do this since we can't set the value before the user's constructor runs (at least for accessor without standard decorators).

The default value is returned from the getter when it would otherwise return undefined and the setter for the property has been called (this is recorded via a set). Maintaining this set is a slight tax/complication.

Concerns and Alternatives

  • Since this approach requires both the setter and the getter to be wrapped, these must be separately decorated with standard decorators (by spec). Typescript doesn't allow this when experimental decorators are in use, so this is a migration concern.
  • Alternatively the initializer could be used as the default value. This would avoid writing the getter and instead just use the normal initializer for it. This would require capturing the default value on the first set call on the property. The concern here is that the storage would be on the instance rather than property options and therefore consume excess memory. Note, while it could technically be on property options, doing so could have unexpected behavior since data would be implicitly shared between instances.
  • Note also, non-primitive values as defaults doesn't really make sense with the default in property options since the default value is shared between instances. To address this, with either the current defaultValue approach or the capturing initializer approach, we could throw/warn or just not use a non-primitive value.
  • For defaultValue, ideally, we'd like to warn if the initializer is used, but it's unclear how to detect this case in general.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants