Tags: web-dev concept

Component Models

Date: 2026-08-19


Every framework since 2013 has agreed on the same core bargain: a component is a function from state to markup, and you compose those functions instead of manipulating the page. Almost everything else — syntax, reactivity, lifecycle — is a variation on that one idea.


A component model is a framework’s answer to three questions: what a unit of UI is, how data gets into it, and how it composes with others.

The shared answer:

        props ──────▶┌──────────────┐
                     │              │
   local state ─────▶│  COMPONENT   │──────▶ markup description
                     │              │
       children ────▶└──────────────┘──────▶ events out
                            ▲
                            └── re-runs whenever state or props change

Data flows down, events flow up. A component reads props and cannot write them; to change something it doesn’t own, it calls a function its parent passed in. That single restriction is what makes a tree of components reasonable to debug — when a value is wrong, there is exactly one path to walk up.

What differs between frameworks

Less than the syntax suggests. The same button in three dialects:

// JSX — markup is an expression in JavaScript
function Button({label, onPress}) {
  return <button onClick={onPress}>{label}</button>;
}
<!-- template — markup is a language with JavaScript expressions in it -->
<script setup>defineProps(['label'])</script>
<template><button @click="$emit('press')">{{ label }}</button></template>

The real differences are three:

  • Where the boundary of a “unit” sits — a function, a class, a file, or a compiled tag
  • How re-running is triggered — see Reactivity, which is the genuinely divergent part
  • How children are passed — children as a prop versus named slots, the template equivalent of passing markup into a hole the component leaves for it

Composition, which is the point

Three mechanisms, in rough order of how often they’re the right answer:

  • Children / slots — the component wraps content it knows nothing about. A Card that renders a border and a shadow should never know what’s inside it
  • Props as configuration — the component branches on values it’s given
  • Inversion — passing behaviour, not data — the parent supplies a function or a component, and the child decides when to call it. This is what Headless Components take to its conclusion

The failure mode is configuration creep. A Card grows hasHeader, headerAlign, footerVariant, showDivider — twelve booleans encoding a layout the caller could have expressed as markup. Every one is a permanent public commitment; see Component API Design and Composition over Configuration.

Where the model leaks

Components are not free. Each one is a boundary the framework tracks, a place re-rendering can start, and — with server rendering — a unit of work done twice, once per environment. Decomposing until every <div> has its own file buys readability and pays in indirection.

The tree is not the DOM. Component nesting and rendered element nesting diverge constantly: a component may render nothing, render into a different part of the document via a portal, or render a fragment with no wrapper at all. Debugging visually while thinking in components is why framework devtools exist as a separate tree.

Identity is positional, not nominal. Frameworks decide whether a component is “the same one as last render” by its position in the tree and its key — not by what it contains. Reorder a list without stable keys and state attaches to the wrong row: the classic symptom is a checkbox that follows the wrong item after a sort.

The line worth holding

A component model is a reuse mechanism dressed as a rendering mechanism. Frameworks sell the rendering; the durable value is that a well-drawn component is the smallest thing two teams can agree on — which is why Design Systems are built out of them and not out of stylesheets.