lit icon indicating copy to clipboard operation
lit copied to clipboard

Wrapper triggers the setter on each re-render

Open maxpatiiuk opened this issue 6 months ago • 3 comments

Which package(s) are affected?

React (@lit/react)

Description

Lit React wrapper triggers the web component setter on each re-render even if the value did not change since the last render. In comparison, without wrappers, both React 18 and React 19 only re-trigger the web component setter if the value changed. Lit React wrapper is inconsistent with React behavior.

https://github.com/lit/lit/blob/fbda6d7b42b8acd19b388e9de0be3521da6b58bb/packages/react/src/create-component.ts#L265

Reproduction

Open console. See that wrapper triggers setter on each re-render. Without wrapper, setter only triggered if value changed:

https://lit.dev/playground/#sample=examples%2Freact-basics&project=W3sibmFtZSI6ImFwcC50c3giLCJjb250ZW50IjoiaW1wb3J0IFJlYWN0IGZyb20gJ2h0dHBzOi8vZXNtLnNoL3JlYWN0QDE4JztcbmltcG9ydCB7Y3JlYXRlUm9vdH0gZnJvbSAnaHR0cHM6Ly9lc20uc2gvcmVhY3QtZG9tQDE4L2NsaWVudCc7XG5pbXBvcnQge2NyZWF0ZUNvbXBvbmVudH0gZnJvbSAnQGxpdC9yZWFjdCc7XG5pbXBvcnQge0RlbW9HcmVldGluZyBhcyBEZW1vR3JlZXRpbmdXQ30gZnJvbSAnLi9kZW1vLWdyZWV0aW5nLmpzJztcblxuLy8gQ3JlYXRlcyBhIFJlYWN0IGNvbXBvbmVudCBmcm9tIGEgTGl0IGNvbXBvbmVudFxuY29uc3QgRGVtb0dyZWV0aW5nID0gY3JlYXRlQ29tcG9uZW50KHtcbiAgcmVhY3Q6IFJlYWN0LFxuICB0YWdOYW1lOiAnZGVtby1ncmVldGluZycsXG4gIGVsZW1lbnRDbGFzczogRGVtb0dyZWV0aW5nV0MsXG59KTtcblxuY29uc3Qgcm9vdCA9IGNyZWF0ZVJvb3QoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2FwcCcpISk7XG5cbnJvb3QucmVuZGVyKDxSb290IC8-KTtcbmZ1bmN0aW9uIFJvb3QoKSB7XG4gIGNvbnN0IFtzdGF0ZSwgc2V0U3RhdGVdID0gUmVhY3QudXNlU3RhdGUoZmFsc2UpO1xuICBSZWFjdC51c2VFZmZlY3QoKCkgPT4ge3NldEludGVydmFsKCgpID0-IHNldFN0YXRlKCh2YWx1ZSkgPT4gIXZhbHVlKSwgMTAwMCl9LCBbXSk7XG5cbiAgY29uc29sZS5sb2coXCJyZS1yZW5kZXJcIik7XG4gIHJldHVybiA8PlxuICAgPERlbW9HcmVldGluZyBuYW1lPVwiV3JhcHBlclwiIC8-XG4gICA8ZGVtby1ncmVldGluZyBuYW1lPVwiTm8gV3JhcHBlclwiIC8-XG4gIDwvPjtcbn0ifSx7Im5hbWUiOiJkZW1vLWdyZWV0aW5nLnRzIiwiY29udGVudCI6ImltcG9ydCB7aHRtbCwgY3NzLCBMaXRFbGVtZW50fSBmcm9tICdsaXQnO1xuaW1wb3J0IHtjdXN0b21FbGVtZW50LCBwcm9wZXJ0eX0gZnJvbSAnbGl0L2RlY29yYXRvcnMuanMnO1xuQGN1c3RvbUVsZW1lbnQoJ2RlbW8tZ3JlZXRpbmcnKVxuZXhwb3J0IGNsYXNzIERlbW9HcmVldGluZyBleHRlbmRzIExpdEVsZW1lbnQge1xuICBfbmFtZSA9IGBgO1xuICBAcHJvcGVydHkoKVxuICBzZXQgbmFtZShuYW1lOnN0cmluZykge1xuICAgIGNvbnNvbGUubG9nKFwic2V0dGVyIHRyaWdnZXJlZCwgb2xkIHZhbHVlOiBcIix0aGlzLl9uYW1lLCBcIi4gbmV3IHZhbHVlOiBcIiwgbmFtZSk7XG4gICAgdGhpcy5fbmFtZSA9IG5hbWU7XG4gIH1cbiAgZ2V0IG5hbWUoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gdGhpcy5fbmFtZTtcbiAgfVxuXG4gIHJlbmRlcigpIHsgcmV0dXJuIHRoaXMubmFtZTsgfVxufVxuXG5kZWNsYXJlIGdsb2JhbCB7XG4gIGludGVyZmFjZSBIVE1MRWxlbWVudFRhZ05hbWVNYXAge1xuICAgICdkZW1vLWdyZWV0aW5nJzogRGVtb0dyZWV0aW5nO1xuICB9XG59XG4ifSx7Im5hbWUiOiJyZWFjdC5kLnRzIiwiY29udGVudCI6ImRlY2xhcmUgbW9kdWxlICdodHRwczovL2VzbS5zaC9yZWFjdEAxOCcge1xuICBleHBvcnQgZGVmYXVsdCBSZWFjdDtcbn1cblxuZGVjbGFyZSBtb2R1bGUgJ2h0dHBzOi8vZXNtLnNoL3JlYWN0LWRvbUAxOC9jbGllbnQnIHtcbiAgZXhwb3J0ICogZnJvbSAncmVhY3QtZG9tL2NsaWVudCc7XG59IiwiaGlkZGVuIjp0cnVlfSx7Im5hbWUiOiJpbmRleC5odG1sIiwiY29udGVudCI6Ik9wZW4gY29uc29sZS4gU2VlIHRoYXQgd3JhcHBlciB0cmlnZ2VycyBzZXR0ZXIgb24gZWFjaCByZS1yZW5kZXIuIFdpdGhvdXQgd3JhcHBlciwgc2V0dGVyIG9ubHkgdHJpZ2dlcmVkIGlmIHZhbHVlIGNoYW5nZWQuPGJyLz5cbjxzY3JpcHQgdHlwZT1cIm1vZHVsZVwiIHNyYz1cIi4vYXBwLmpzXCI-PC9zY3JpcHQ-PGRpdiBpZD1cImFwcFwiPjwvZGl2PlxuIn0seyJuYW1lIjoicGFja2FnZS5qc29uIiwiY29udGVudCI6IntcbiAgXCJkZXBlbmRlbmNpZXNcIjoge1xuICAgIFwibGl0XCI6IFwiXjNcIixcbiAgICBcIkB0eXBlcy9yZWFjdFwiOiBcIjE4LjIuN1wiLFxuICAgIFwiQHR5cGVzL3JlYWN0LWRvbVwiOiBcIl4xOFwiLFxuICAgIFwiQHR5cGVzL3JlYWN0LWRvbS9jbGllbnRcIjogXCJeMThcIlxuICB9XG59IiwiaGlkZGVuIjp0cnVlfV0

Workaround

  1. React component needs to set properties manually in a use effect (bad ux for consumer)
  2. Update to react 19 and don't use the wrapper. However, some enterprise and LTS customers will be stuck on react 18 for the next 2-3 years.
  3. The web components needs to diff the value inside the setter - requires a lot of boilerplate:
    • Is it Lit's recommendation that every component setter must always diff manually against the old value?
    • This seems redundant with the hasChanged() concept. And it is not trivial for the setter to get a reference to its hasChanged() function.
    • Even besides that, the hasChanged() concept is not sufficient as it compares against the getter value, rather than against the value that was set by the user. Imagine the component auto-casts a string to an object value in the setter.
      • Ideally the setter should not trigger again if it is triggered again with the same string value - doing so is not trivial as each setter now needs two backing properties (the backing value and the raw user-set value). That is why LIt's wrapper behavior is problematic.

Issues with current wrapper behavior: needless work on re-render. causes infinite re-loading in some components. requires a lot of boilerplate to work around in web component.

Is this a regression?

No or unsure. This never worked, or I haven't tried before.

Affected versions

1.0.7

Browser/OS/Node environment

Chrome: 137.0.7151.69 (Official Build) (arm64)

maxpatiiuk avatar Jun 09 '25 17:06 maxpatiiuk

is it really a big problem that the setter is invoked?

based on the comment in https://github.com/lit/lit/blob/fbda6d7b42b8acd19b388e9de0be3521da6b58bb/packages/react/src/create-component.ts#L174-L175 this seems to be the intended behavior, where the web component itself is responsible for handling property sets that shouldn't update (which LitElement based components do).

i think the existing behavior is better for cases where the property value potentially drifts where something besides the React component updates the web component's property, then a re-render from React will force it back to the bound value.

though considering React 19's behavior seems to be to allow the drift, it might be reasonable to do that dirty checking in the commented line above based on the previously bound value. i'd think that's potentially a breaking change for the wrapper though.

augustjk avatar Jun 10 '25 03:06 augustjk

is it really a big problem that the setter is invoked?

As more people update to react 19, this is becoming less of an issue. But yes, our setter auto-casts string to a complex object and triggers a network request and ui loading in response. The complex object does not retain reference to the original string value so manual diffing is not trivial (we would need to create a separate property just to store raw user's value for each such affected @property)

i'd think that's potentially a breaking change for the wrapper though

Makes sense. Given that the need for react wrappers is on the way out, probably not worth it. Still unfortunate that lit's react wrappers behavior differs from react 18 and react 19 defaults

maxpatiiuk avatar Jun 13 '25 03:06 maxpatiiuk

For Lit-based web components, we assumed that they're already doing change detection because they're used outside of systems like React that do their own dirty checking. We din't want to layer on two change detection passes unnecessarily.

justinfagnani avatar Jun 13 '25 08:06 justinfagnani