Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Controlled components can lead to an inconsistent state between Vue and DOM #13237

Closed
maartenbreddels opened this issue Feb 27, 2025 · 3 comments

Comments

@maartenbreddels
Copy link

Context

In React it's very common to have a controlled component. e.g. like this https://codesandbox.io/p/devbox/react-dev-forked-85xg4s where the state completely lives outside of the child component.

Although I don't think in the VueJS ecosystem uses the same wording, I have seen this pattern being used (e.g. https://stackoverflow.com/questions/68496743/vue-js-input-value-not-reflecting-value-in-component-data/79472786 which also demonstrates the issue I'm about to describe).
More indirect versions of this are where an event gets forwarded to a Vuex store, and the component (should) show the resulting state from the store.

Demonstration of bug

The simplest example that demonstrates this pattern is:

<div id="app">
  <h2>controll text field can lead to inconsistent state</h2>
  <input type="text" :value="name" @input="handleInput"/>
  force update:
  <input type="checkbox" v-model="forceUpdateWorkaround"/>
</div>
new Vue({
  el: "#app",
  data: {
    name: 'foo',
    forceUpdate: false
  },
  methods: {
    handleInput: function (e) {
      console.log("e.target.value =", e.target.value)
      this.name = e.target.value.lower()
      if (this.forceUpdate) {
        this.$forceUpdate()
      }
    },
  }
})

(full example here

When typing 'b' in the text field, it shows 'FOOB' in all caps, which mirrors the value data. However, when we replace the last B' with a 'b', the text field will change to 'FOOb', but the event handler (handleInput`) will not change the vue model, and therefore not update the component. Now, the internal data model and the DOM are inconsistent

Discussion

This is a rare and subtle (and therefore, I think dangerous) bug that can result from this pattern. ReactJS does not have this problem (see above example). What I think ReactJS does is that it will update the DOM with the vDOM for the DOM element that triggered the event. I think this makes a lot of sense since DOM element events can be the source of internal state changes (like the value of a text field), but those events do not always trigger re-renders in the frameworks (since state changes may not happen, as demonstrates above). In this edge case, a state change in a DOM element without an associated state change in the VueJS framework can cause inconsistencies.

Some background

We hit the same issues in a Python framework called Solara which is similar to React, and we were comparing how other frameworks solve this problem. We found that SolidJS and VueJS have this issue, while ReactJS does not. In the end, we use a solution similar to ReactJS in our underlying framework: widgetti/reacton#45

Proposed solution

VueJS should always update the DOM element that triggered an event if, after handling its event handlers, it did not trigger a rerender.

@Elphazy
Copy link

Elphazy commented Mar 2, 2025

Instead of using :value and @input, using v-model directly for two-way binding might prevent inconsistencies.

@maartenbreddels
Copy link
Author

That is a common 'workaround', but it will trigger a change with an invalid value. For instance, in the above example, it will trigger both 'FOOb' and 'FOOB'. I don't think if you do proper state management that is acceptable.

But more importantly, using the pattern of value + onChange usually work. So if you use this pattern throughout your app, you might feel confident your app is without bugs, and then in this really strange edge case, your app is in an inconsistent state.

@Justineo
Copy link
Member

Justineo commented Mar 3, 2025

F.Y.I. vuejs/rfcs#188

BTW: This (vuejs/vue) is the repo for Vue 2, which is no longer maintained by the core team. Let's move to the RFC repo for this kind of discussions.

@Justineo Justineo closed this as completed Mar 3, 2025
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

No branches or pull requests

3 participants