Reactivity Edge Cases

Module 2 established that Vue 2 converts data properties into getter/setters via Object.defineProperty at initialization time. Two categories of mutation fall outside what that mechanism can observe, and Vue 2 documents them explicitly as known limitations — not bugs, but structural consequences of the design.

new-property-problem.jsjavascript
export default {
  data() {
    return {
      product: { name: 'Widget', stock: 5 }
      // 'discount' does NOT exist here
    }
  },
  methods: {
    applyDiscount() {
      // BROKEN: silently non-reactive. Template referencing
      // product.discount will never update.
      this.product.discount = 0.1

      // FIX 1 — Vue.set / vm.$set: installs the getter/setter retroactively
      this.$set(this.product, 'discount', 0.1)

      // FIX 2 — replace the whole object (new object = full walk of its keys)
      this.product = { ...this.product, discount: 0.1 }
    }
  }
}
array-index-problem.jsjavascript
export default {
  data() {
    return { items: ['a', 'b', 'c'] }
  },
  methods: {
    brokenUpdate() {
      this.items[0] = 'z'      // BROKEN: not reactive, template won't update
      this.items.length = 1     // BROKEN: not reactive either
    },
    fixedUpdate() {
      this.$set(this.items, 0, 'z')      // FIX: Vue.set understands array indices
      this.items.splice(1)                // FIX: splice is a patched mutator method
    }
  }
}
OperationReactive?Why
items.push(x) / .pop() / .splice()YesVue patches these 7 Array.prototype methods on the instance's array
items[i] = xNoPlain index assignment, no getter/setter installed per-index
items.length = nNoLength assignment doesn't route through any patched method
Vue.set(items, i, x) / this.$set(...)YesExplicitly calls the patched splice internally to notify dependents
obj.newKey = valueNoObject.defineProperty was never called for a key that didn't exist at init
Vue.set(obj, 'newKey', value)YesExplicitly walks the new key through defineReactive
delete obj.keyNoPlain delete doesn't notify the dependency tracker
Vue.delete(obj, 'key') / this.$delete(...)YesRemoves the key and explicitly triggers dep.notify()

Render Functions & the Virtual DOM

Templates compile down to render functions that return VNodes — plain JavaScript objects describing what the DOM should look like. Writing render(h) by hand skips the template compiler entirely, useful for components whose output structure can't be expressed cleanly in template syntax (deeply dynamic tag names, heavy conditional branching, performance-critical list rendering).

vnode-shape.jsjavascript
// Simplified shape of what h() (createElement) returns —
// this is what your <template> compiles into behind the scenes.
const vnode = {
  tag: 'div',
  data: { staticClass: 'card', attrs: { id: 'product-1' } },
  children: [
    { tag: 'span', data: {}, children: undefined, text: 'Widget' }
  ],
  text: undefined,
  elm: undefined,     // populated with the real DOM node after patch
  key: undefined,
  componentOptions: undefined // populated instead of tag/children for component vnodes
}
FunctionalBadge.jsjavascript
// Functional component: no instance, no `this`, no reactivity overhead —
// pure function of props+context to VNode. Ideal for presentational leaves
// rendered in bulk (table cells, list badges).
export default {
  functional: true,
  props: {
    status: { type: String, required: true }
  },
  render(h, context) {
    const { status } = context.props
    const colorMap = { active: '#16a34a', paused: '#d97706', archived: '#6b7280' }
    return h(
      'span',
      {
        staticClass: 'badge',
        style: { background: colorMap[status] ?? '#6b7280' }
      },
      context.children ?? status
    )
  }
}

Practical Example: High-Performance Data Table via Manual render(h)

A table rendering thousands of rows through a template is workable, but a hand-written render function avoids per-row template overhead and gives full control over VNode reuse and key assignment for virtualization.

PerformantDataTable.jsjavascript
export default {
  name: 'PerformantDataTable',
  props: {
    columns: { type: Array, required: true }, // [{ key, label, formatter? }]
    rows: { type: Array, required: true },
    rowHeight: { type: Number, default: 36 },
    visibleCount: { type: Number, default: 20 } // simple windowing, not full virtual scroll
  },
  data() {
    return { scrollTop: 0 }
  },
  computed: {
    startIndex() {
      return Math.max(0, Math.floor(this.scrollTop / this.rowHeight) - 2)
    },
    visibleRows() {
      return this.rows.slice(this.startIndex, this.startIndex + this.visibleCount)
    },
    totalHeight() {
      return this.rows.length * this.rowHeight
    },
    offsetY() {
      return this.startIndex * this.rowHeight
    }
  },
  methods: {
    onScroll(e) {
      this.scrollTop = e.target.scrollTop
    },
    renderHeaderRow(h) {
      return h(
        'div',
        { staticClass: 'pdt__row pdt__row--header' },
        this.columns.map((col) =>
          h('div', { key: col.key, staticClass: 'pdt__cell' }, col.label)
        )
      )
    },
    renderBodyRow(h, row) {
      return h(
        'div',
        { key: row.id, staticClass: 'pdt__row', style: { height: this.rowHeight + 'px' } },
        this.columns.map((col) => {
          const raw = row[col.key]
          const value = col.formatter ? col.formatter(raw) : raw
          return h('div', { key: col.key, staticClass: 'pdt__cell' }, String(value))
        })
      )
    }
  },
  render(h) {
    return h('div', { staticClass: 'pdt', on: { scroll: this.onScroll } }, [
      this.renderHeaderRow(h),
      h('div', { staticClass: 'pdt__viewport', style: { height: this.totalHeight + 'px', position: 'relative' } }, [
        h(
          'div',
          { style: { transform: `translateY(${this.offsetY}px)`, position: 'absolute', width: '100%' } },
          this.visibleRows.map((row) => this.renderBodyRow(h, row))
        )
      ])
    ])
  }
}
PerformantDataTable.style.vuevue
<style scoped>
.pdt { max-height: 480px; overflow-y: auto; border: 1px solid #ddd; font-family: system-ui, sans-serif; }
.pdt__row { display: flex; }
.pdt__row--header { position: sticky; top: 0; background: #f8f8f8; font-weight: 600; z-index: 1; }
.pdt__cell { flex: 1; padding: 0.5rem 0.75rem; border-bottom: 1px solid #eee; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
</style>