lit
lit copied to clipboard
Wrapper triggers the setter on each re-render
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
- React component needs to set properties manually in a use effect (bad ux for consumer)
- 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.
- 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)
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.
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
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.