Edit in JSFiddle

function reactive(obj, callback) {
  const wrapped = {}

  for (let key in obj) {
    let value = obj[key]

    if (typeof value === 'object' && !Array.isArray(value)) {
      value = reactive(value, callback)
    }
    Object.defineProperty(wrapped, key, {
      get: function reactiveGetter() {
        return value
      },
      set: function reactiveGetter(newValue) {
        console.log('setting value from', value, 'to', newValue)
        value = newValue
        callback()
      }
    })
  }

  return wrapped
}

function createComponent (obj) {
  const data = obj.data || {}
  const render = obj.render.bind(obj)
  obj.data = reactive(data, render)
  
  return obj
}

const helloComponent = createComponent({
  el: document.getElementById('content'),
  data: {
    name: {
      first: 'Hansi',
      last: 'Hinterseer'
    }
  },
  render: function () {
    this.el.innerHTML = `<h1>Hello ${this.data.name.first} ${this.data.name.last}</h1>`
  }
})

const lipsumComponent = createComponent({
  el: document.getElementById('content2'),
  data: {
    content: []
  },
  render: function () {
    console.log(this.data)
    const elements = this.data.content.map(function(el) {
      const tag = el[0], text = el[1]
      return `<${tag}>${text}</${tag}>`
    })
    this.el.innerHTML = elements.join('')
  }
})

helloComponent.data.name.first = 'Ada'
helloComponent.data.name.last = 'Lovelace'
lipsumComponent.data.content = [
  ['p', 'Lorem Ipsum sit amet dolor…'],
  ['h2', 'DOLOR!']
]
lipsumComponent.data.content.push(['p', 'Hereby I kindly declare that I am out of ideas!'])
The classic:

<div id="content">…loading</div>
<div id="content2"></div>