I wonder how many people have attempted to build what they want starting from an empty JS file.

I think most people are scared of what they will find.

That the decades they have spent twisting themselves around frameworks and learning all these abstractions, is not actually necessary at all.

We already have a really nice _declarative_ abstraction in the form of HTML/CSS.

I don't know why we decided to cram everything into the view layer.

Separate your logic into a view model, and then data bind to it.

A simple data binding framework is very easy to write. And then you have full control over everything!

Compare the size of the code below and how easy it is to debug/trace....to the enormous React/Angular/etc. codebases.

const bindings = [] // {elementId, modelId, componentId}

// Allows looking up functions by strings, that are referenced in server-side code.
// Normally we could just reference function objects in memory, but in two separate environments we must use strings.
const components = {}

// Model
////////////////////

const models = [{id: shortId(), count: 1}]

const model = models[0]

// Server-side render route
////////////////////

function renderPage() {

// Create view
const div = createCounterComponent()
document.body.appendChild(div)

// Send this to the client.
const html = document.documentElement.outerHTML
const ssrScriptEl = document.createElement('ssr')
ssrScriptEl.textContent = JSON.stringify({models, bindings})
document.body.appendChild(ssrScriptEl)

return new Response(html)
}

// Client-side main function
////////////////////

function clientMain() {

window.ssr = JSON.parse(document.getElementById("ssr").textContent)

rehydrate(models, bindings)

// Update model and notify view.
model.name = 'goodbye'
onModelChange(model.id)

}

function rehydrate(models, bindings) {
for (const binding of bindings) {
const {modelId, elementId, componentId} = binding
const element = document.getElementById(elementId)
const component = component[componentId]
const {setup, render} = component
setup(element, model)
}
}

function onModelChange(changedModelId) {
const affectedBindings = window.ssr.models.filter( ({elementId, modelId}) => modelId === changedModelId )
for (const binding of affectedBindings) {
const {elementId, render} = binding
const element = document.getElementById(elementId)
render(element, model)
}
}

// Component
////////////////////

function createCounterComponent(model) {
const div = createElement('div', model, createCounterComponent)
return div
}

// Doesn't run during SSR rehydration.
function render(element, model) {
element.innerHTML = model.count
}

// Does run during SSR hydration.
function setup(element, model) {
element.onClick = () => { model.count++ }
}

createCounterComponent.setup = setup
createCounterComponent.render = render
registerComponent(createCounterComponent)

function registerComponent(component) {
const componentId = component.name
components[componentId] = component
}

////////////////////

function createElement(elementType, model, componentFunction) {
const {render, setup} = componentFunction
const componentId = componentFunction.name

const element = document.createElement(elementType)
const elementId = shortId()
element.id = elementId

const modelId = model.id

bindings.push({elementId, modelId, componentId})

render(element, model)

return element
}