Vue: Checkboxes don't keep explicitly declared checked state

Created on 22 Jan 2018  ·  8Comments  ·  Source: vuejs/vue

Version

2.5.13

Reproduction link

https://jsfiddle.net/71puwq8k/ (component version)
https://jsfiddle.net/eznbzfdw/ (without component)

Steps to reproduce

Toggle any of the checkboxes a few times

What is expected?

Any checkbox should always stay checked after toggling it.

What is actually happening?

As you can see in my example I manually bind change event and checked attribute to the input, because I would like to have control over its checked state. But even though I always force the value to be true, the checkbox always toggles between checked/unchecked, so it makes our template and the data out of sync. Even calling $forceUpdate() does not help in this case.

improvement

Most helpful comment

This is technically the expected behavior although I admit it can be confusing to some extent.

Vue's reactivity system will only trigger a re-render if the state has actually changed. In this case, you are setting a value to true, but it is already true, so Vue figures nothing needs to update and does not perform any.

Another aspect of the problem is that you are ignoring whatever is happening in the DOM (the fact that the state of the element has been modified by the user) and not reflecting it back into your state. So the checkbox has been unchecked but your state hasn't been updated.

So, one way to do it is what @Justineo suggested: explicitly sync the DOM state back into your app state first, before resetting it.

Another way is let v-model do that for you:

<input v-model="state[key]" @change="onToggleState(key)" type="checkbox">

Note in this case because v-model always syncs the DOM state back into app state before your handler, it would trigger an update when necessary. In this case your onToggleState handler can simply be:

onToggleState(key) {
  this.state[key] = true
}

In React's case - setState actually does not perform the value check, it always results in an update. Calling setState in React is similar to mutating state + calling this.$forceUpdate.

All 8 comments

Even calling $forceUpdate() does not help in this case.

Hmm apparently I made a mistake here. In component version i guess i need to call $forceUpdate() on specific comoponent, not the $root instance. It fixes the issue in the version without custom component but it still seems off to me that i need to even call $forceUpdate() to make template re-render 🤔

As a workaround for the moment, you can change the value to force a render but that only works on the non-component approach.
edit: another workaround would be to disable the checkboxes if they cannot be unticked
The easiest one is to force the checked status of the checkbox (event.target.checked = this.state[key]) (works in both cases)

I'm unsure we can do something for the component scenario since the browser is toggling the state but then we make sure the prop doesn't change and therefore the component never rerenders.
And I'm afraid it may be the same scenario for the non-component version as we don't render because no state changed 🤔

The only workaround in my mind is to manually “trigger” the data change:

onToggleState (key, event) {
  const { checked } = event.target
  this.state[key] = checked

  this.$nextTick(() => {
    this.state[key] = true
  })
}

Setting the checked value on the checkbox after setting the value seems to work in all scenarios 🙂

event.target.checked = this.state[key]

(I missed the “without touching DOM” part in my previous comment...) 😄

Hi @posva thank you for the solution, settings event.target[prop] indeed fixes the issue, but yeah I also wonder whether it can be improved inside Vue (React somehow does it). If not, can we at least explain it to users in the docs? Because this current behaviour is very confusing and I have to say even I who's working with Vue for almost 2 years now i couldn't figure this out :D

Here is another example where in my opinion, user can easily fall into this trap: https://jsfiddle.net/fdnjo5f2/

It's a simple text field where I want to control the value that this field can accept (0-100 in that case). Now, to reproduce the issue follow the steps:

  • Set a new value to 200 (value goes back to 100 as expected)
  • Set 200 again, this time it stays at 200 and not 100 as we would expect

This is technically the expected behavior although I admit it can be confusing to some extent.

Vue's reactivity system will only trigger a re-render if the state has actually changed. In this case, you are setting a value to true, but it is already true, so Vue figures nothing needs to update and does not perform any.

Another aspect of the problem is that you are ignoring whatever is happening in the DOM (the fact that the state of the element has been modified by the user) and not reflecting it back into your state. So the checkbox has been unchecked but your state hasn't been updated.

So, one way to do it is what @Justineo suggested: explicitly sync the DOM state back into your app state first, before resetting it.

Another way is let v-model do that for you:

<input v-model="state[key]" @change="onToggleState(key)" type="checkbox">

Note in this case because v-model always syncs the DOM state back into app state before your handler, it would trigger an update when necessary. In this case your onToggleState handler can simply be:

onToggleState(key) {
  this.state[key] = true
}

In React's case - setState actually does not perform the value check, it always results in an update. Calling setState in React is similar to mutating state + calling this.$forceUpdate.

Thank you. I appreciate the explanation 👍

Was this page helpful?
0 / 5 - 0 ratings

Related issues

paceband picture paceband  ·  3Comments

franciscolourenco picture franciscolourenco  ·  3Comments

hiendv picture hiendv  ·  3Comments

gkiely picture gkiely  ·  3Comments

bdedardel picture bdedardel  ·  3Comments