Fix: Cannot Mutate Script Setup Binding From Options API in Vue.js

Error message:
Cannot mutate <script setup> binding "{key}" from Options API.
Composition API 2025-01-25

What Causes This Warning?

This warning occurs when you try to modify a value defined in <script setup> from the Options API (like methods, computed, or watchers defined in a separate <script> block).

The Problem

<script setup>
const count = ref(0)
</script>

<script>
export default {
  methods: {
    // ❌ Trying to mutate script setup binding
    increment() {
      this.count++ // Warning!
    }
  }
}
</script>

The Fix

Keep All Logic in Script Setup

<script setup>
import { ref } from 'vue'

const count = ref(0)

// ✅ Define methods in script setup
function increment() {
  count.value++
}
</script>

Use Options API Entirely

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      count.value++
    }

    return { count, increment }
  }
}
</script>

Common Scenarios

Mixing for Legacy Reasons

<!-- ❌ Mixing APIs incorrectly -->
<script setup>
const formData = reactive({
  name: '',
  email: ''
})
</script>

<script>
export default {
  methods: {
    submitForm() {
      // Can't access formData here properly
      this.formData.name = 'test' // Warning!
    }
  }
}
</script>

<!-- ✅ Keep everything in script setup -->
<script setup>
import { reactive } from 'vue'

const formData = reactive({
  name: '',
  email: ''
})

async function submitForm() {
  await api.submit(formData)
}
</script>

Using with defineOptions

<script setup>
import { ref } from 'vue'

// ✅ Use defineOptions for component options
defineOptions({
  name: 'MyComponent',
  inheritAttrs: false
})

const count = ref(0)
const increment = () => count.value++
</script>

Lifecycle Hooks

<!-- ❌ Don't mix lifecycle approaches -->
<script setup>
const data = ref(null)
</script>

<script>
export default {
  mounted() {
    this.data = 'loaded' // Warning!
  }
}
</script>

<!-- ✅ Use onMounted in script setup -->
<script setup>
import { ref, onMounted } from 'vue'

const data = ref(null)

onMounted(() => {
  data.value = 'loaded'
})
</script>

Computed Properties

<!-- ❌ Computed in Options API accessing script setup -->
<script setup>
const firstName = ref('John')
const lastName = ref('Doe')
</script>

<script>
export default {
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName // Issues!
    }
  }
}
</script>

<!-- ✅ Use computed in script setup -->
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>

When You Need Both Script Blocks

The only valid reason to have both is for component options not available in <script setup>:

<script>
// For options not available in script setup
export default {
  name: 'MyComponent',
  inheritAttrs: false,
  customOption: 'value'
}
</script>

<script setup>
// All reactive code here
const count = ref(0)
</script>

But prefer defineOptions (Vue 3.3+):

<script setup>
defineOptions({
  name: 'MyComponent',
  inheritAttrs: false
})

const count = ref(0)
</script>

Quick Checklist

  • Don’t mix <script setup> bindings with Options API methods
  • Keep all reactive logic in <script setup>
  • Use defineOptions for component options (Vue 3.3+)
  • Use Composition API lifecycle hooks (onMounted, etc.)
  • If you need both scripts, only use second script for options