flow-vanilla / starter · 00

Learn the
foundation
first.

Before writing a single Flow-Arch component, you need to understand four concepts. This page is your starting point.

Each section explains one idea, shows you a minimal working code example, and links directly to the MDN official documentation. Take your time. These concepts compound.

What you will learn
  • What Web Components are and why they matter
  • What Shadow DOM does and why isolation is powerful
  • What a Pure Function is and why it eliminates bugs
  • What Declarative style means vs Imperative
  • Why Flow-Arch combines all four — and what you gain
01 — Foundation

Web Components

A Web Component is a custom HTML element you define yourself — built directly into the browser, with no library required.

Instead of <div class="timer"> everywhere, you write <flow-timer></flow-timer> — and the browser knows exactly what to render because you taught it.

MDN — Web Components Write a Web Component
Key Idea

Web Components are not a framework. They are a browser standard. They work in every modern browser, with zero dependencies.

Minimal Example

hello-world web component JS
// Step 1: Define a class that extends HTMLElement
class HelloWorld extends HTMLElement {

  // connectedCallback runs when the element
  // is inserted into the DOM
  connectedCallback() {
    this.innerHTML = '<p>Hello, Web Component!</p>'
  }
}

// Step 2: Register your custom tag name
// Tag names MUST contain a hyphen (-)
customElements.define('hello-world', HelloWorld)

// Step 3: Use it in HTML like any normal tag
// <hello-world></hello-world>
Without Web Components
  • Requires React / Vue runtime
  • npm install + build step
  • Framework lock-in
  • Styles leak everywhere
  • Non-standard HTML
With Web Components
  • Zero dependencies
  • Runs directly in browser
  • Standard HTML element
  • Works with any framework
  • Built-in lifecycle hooks
02 — Foundation

Shadow DOM

Shadow DOM is a browser feature that creates a private, isolated DOM tree inside your Web Component. Styles and structure cannot leak in or out.

Think of it as a sealed capsule. What happens inside stays inside. External CSS cannot break your component. Your component's CSS cannot break the rest of the page.

MDN — Using Shadow DOM Shadow DOM step by step
Key Idea

Shadow DOM gives you true encapsulation. Not naming conventions like BEM. Not scoped CSS from a build tool. Real, browser-enforced isolation.

Minimal Example

shadow dom encapsulation JS
class ShadowCard extends HTMLElement {
  constructor() {
    super()

    // attachShadow creates the private DOM tree
    // mode: "open" means JS outside can still query it
    this.attachShadow({ mode: "open" })

    // Styles here ONLY affect this component
    // They will never leak to the outside page
    this.shadowRoot.innerHTML = `
      <style>
        p { color: red; font-size: 24px; }
      </style>
      <p>I am red and large</p>
    `
  }
}

customElements.define('shadow-card', ShadowCard)

// Any <p> tags outside this component
// are completely unaffected by the style above
Without Shadow DOM
  • CSS leaks between components
  • Naming collisions (.btn, .card)
  • Global styles override yours
  • Need BEM or CSS Modules
  • Unpredictable at scale
With Shadow DOM
  • Styles are truly isolated
  • No naming conventions needed
  • Component is self-contained
  • Predictable at any scale
  • Browser-enforced, not a hack
03 — Foundation

Pure Functions

A pure function has two rules: same input always gives same output, and it never touches anything outside itself.

That's it. Two rules. But following them eliminates an entire class of bugs — the hardest ones to find: state bugs, timing bugs, side-effect bugs.

The Two Rules

1. Deterministic — same input → same output, always, forever.
2. No side effects — does not read or write anything outside its parameters.

Pure vs Impure — Side by Side

pure vs impure function JS
// ✗ IMPURE — depends on external variable
// If TAX_RATE changes, this function changes too
const TAX_RATE = 0.1
const calculateTaxImpure = (price) => price * TAX_RATE


// ✓ PURE — all inputs come through parameters
// Same input → same output, always
const calculateTax = (price, rate) => price * rate


// ✗ IMPURE — modifies the original array (mutation)
const addItemImpure = (cart, item) => {
  cart.push(item)   // ← changes the outside world
  return cart
}


// ✓ PURE — returns a new array, original untouched
const addItem = (cart, item) => [...cart, item]


// ✓ PURE REDUCER — the heart of Flow-Arch
// Takes current state + action, returns NEW state
// Never modifies state directly
const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }
    default:
      return state
  }
}
Impure Functions cause
  • Bugs that depend on call order
  • Shared state corruption
  • Impossible to unit test cleanly
  • Hard to reason about
  • Hidden dependencies
Pure Functions give you
  • Predictable, testable logic
  • No hidden state bugs
  • Safe to call anywhere, anytime
  • Easy to read and reason about
  • Composable and reusable
04 — Foundation

Declarative Style

Declarative code describes what you want. Imperative code describes how to get there step by step.

Declarative code is shorter, easier to read, and harder to get wrong — because you are expressing intent, not instructions.

One Sentence

Imperative: "Go to the kitchen, open the fridge, take the milk, pour it."
Declarative: "I want a glass of milk."

Same Task, Two Styles

filter even numbers — imperative vs declarative JS
const numbers = [1, 2, 3, 4, 5, 6]


// ✗ IMPERATIVE — tells the computer HOW to do it
// You manage the loop, the index, the result array
const evensImperative = []
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    evensImperative.push(numbers[i])
  }
}


// ✓ DECLARATIVE — tells the computer WHAT you want
// No index, no mutation, reads like a sentence
const evensDeclarative = numbers.filter(n => n % 2 === 0)


// ✓ DECLARATIVE VIEW — the Flow-Arch way
// Describe what the UI should look like
// given the current state. Nothing more.
const view = (state) => `
  <div class="count">${state.count}</div>
  <button data-action="INCREMENT">+</button>
`
// view() does not DO anything.
// It just DESCRIBES what to show.
05 — Flow-Arch

The Combination

None of these four concepts is new. What Flow-Arch proposes is combining all of them together — as a discipline, in vanilla JS, with zero dependencies.

Each concept solves a different problem. Together, they cover nearly every common source of frontend bugs and complexity.

Problem
  • CSS bleeding between components
  • State bugs from mutation
  • Logic tangled with DOM code
  • Hard to read imperative loops
  • Heavy framework dependency
Flow-Arch Solution
  • Shadow DOM isolation
  • Pure functions + immutability
  • Web Component boundary
  • Declarative view functions
  • Zero dependencies, native APIs
06 — Flow-Arch

The Loop

Every Flow-Arch component runs the same loop. Learn this loop once, understand every component forever.

Start
State
Pure fn
View
User
Action
Pure fn
Reducer
New
State
the complete loop — annotated JS
// ─── STATE ────────────────────────────────────────────
// Plain object. Single source of truth.
// Never modified directly.
const createInitialState = () => ({ count: 0 })


// ─── REDUCER (Pure Function) ──────────────────────────
// Old state + action → new state
// This is where ALL logic lives.
const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT": return { ...state, count: state.count + 1 }
    case "DECREMENT": return { ...state, count: state.count - 1 }
    default:         return state
  }
}


// ─── VIEW (Pure Function) ─────────────────────────────
// State in → HTML string out.
// No logic. No side effects. Just a description.
const view = (state) => `
  <div class="count">${state.count}</div>
  <button data-action="DECREMENT">−</button>
  <button data-action="INCREMENT">+</button>
`


// ─── WEB COMPONENT (Side-effect boundary) ────────────
// The only place where DOM and events live.
// Connects the pure world to the real browser.
class FlowCounter extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: "open" })
    this.state = createInitialState()

    // dispatch = the only way to change state
    this.dispatch = (action) => {
      this.state = reducer(this.state, action)
      this.render()
    }
  }

  connectedCallback() {
    this.render()
    this.shadowRoot.addEventListener("click", (e) => {
      const type = e.target.dataset.action
      if (type) this.dispatch({ type })
    })
  }

  render() {
    this.shadowRoot.innerHTML = view(this.state)
  }
}

customElements.define("flow-counter", FlowCounter)
07 — Flow-Arch

Why It Matters

Combining these four ideas into one discipline gives you properties that no single concept provides alone.

BENEFIT_01

Predictable by Design

Pure reducers mean the same action always produces the same result. No surprises. No race conditions.

BENEFIT_02

Zero Framework Risk

Web Components are a browser standard. Your code will still run in 10 years. No breaking changes from a framework update.

BENEFIT_03

Trivially Testable

Pure functions need no setup, no mocks, no DOM. Test your reducer and view with simple input/output assertions.

BENEFIT_04

True Encapsulation

Shadow DOM ensures your component is a sealed unit. Styles and structure cannot be broken by the outside world.

BENEFIT_05

Readable at a Glance

Declarative view functions describe what the UI looks like. You read them like a spec, not an instruction manual.

BENEFIT_06

Side Effects Contained

All DOM manipulation and async work lives in the Web Component shell. The rest of your code stays pure and clean.

The Core Claim

Flow-Arch does not claim to be the best way to build UIs. It claims to be the most honest way — using only what the browser already gives you, disciplined by ideas borrowed from functional programming.

Everything here is a proposal. An exploration. Not a product.