Skip to content

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

Closed
@maartenbreddels

Description

@maartenbreddels

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions