Honest Documentation

Known
Limitations.

This is a living record of what Flow-Arch cannot do well — yet. Every limitation is an open research question. Honesty about constraints is part of the methodology.

Each entry describes the problem, explains when it matters, shows any current workaround, and frames the open question it leaves behind. Limitations resolved through experiments will be updated here.

Philosophy

A limitation honestly documented is more valuable than a feature dishonestly promoted. If you find something missing from this list, that is a contribution.

ID Limitation Status Priority
L-01 Full innerHTML replacement on every render KNOWN High
L-02 No inter-component state management KNOWN High
L-03 No async action handling KNOWN Medium
L-04 No enforced purity (inherent to JS) KNOWN Low
L-05 String-based view has no type safety KNOWN Medium
L-06 No SSR implementation yet KNOWN Medium
L-07 Component boilerplate repetition KNOWN Low
L-01

Full innerHTML Replacement
on Every Render

KNOWN

What happens

Every time state changes, the entire Shadow DOM is destroyed and rebuilt from scratch.

the current pattern JS
render() {
  // Every render — full replacement
  // Old nodes destroyed. New nodes created.
  this.shadowRoot.innerHTML = view(this.state)
}

Why it is a problem

The browser must destroy all existing nodes, parse the new HTML string, create all new nodes, recalculate styles, re-layout, and repaint — even when only one value changed.

concrete consequences JS
// 1. Input loses focus on every render
// <input> is destroyed and recreated — cursor position lost

// 2. CSS transitions reset
// transition: opacity 0.3s — resets every render because
// the element is a brand new node each time

// 3. Expensive for large lists
// 100 todo items = 100 nodes destroyed + 100 recreated
// on every single keystroke or tick

When it does not matter

Simple components updated on user click (counter, toggle, timer). DOM trees with fewer than ~20 nodes. No <input> elements inside the component.

Open Question

Can we build a minimal DOM diff algorithm in pure vanilla JS, without dependencies, that still fits the Flow-Arch pure function philosophy? What is the minimum viable diffing strategy?

experiments/dom-diff/ — not started yet
L-02

No Inter-Component
State Management

KNOWN

What happens

Each Web Component manages its own private state. There is no shared store or global state layer. Two sibling components cannot share data directly.

Current workaround

Use CustomEvent to bubble data up, and attributes to pass data down. It works but becomes verbose for complex trees.

manual event bus pattern JS
// Child → Parent: dispatch a custom event
this.dispatchEvent(new CustomEvent("state-changed", {
  bubbles: true,
  detail: { count: this.state.count }
}))

// Parent → Child: set attribute
childEl.setAttribute("count", newCount)

// This works. But with 5+ components it becomes
// very hard to trace where data comes from.
Open Question

Can a global store be designed as a pure function system, consistent with Flow-Arch's philosophy, without introducing a framework or complex observer pattern?

experiments/global-store/ — not started yet
L-03

No Async Action Handling

KNOWN

What happens

Reducers are synchronous pure functions. There is no built-in mechanism for async operations like fetch.

async is not allowed in reducers JS
// ✗ This breaks the pure function contract
const reducer = async (state, action) => {
  const data = await fetch("/api/data")  // ✗ side effect
  return { ...state, data }
}

// ✓ Current workaround: async in the component shell
async connectedCallback() {
  this.dispatch({ type: "LOADING" })

  const res  = await fetch("/api/data")
  const data = await res.json()

  // Then dispatch synchronously with the result
  this.dispatch({ type: "DATA_LOADED", payload: data })
}
Trade-off

Keeping reducers synchronous is not a bug — it is a deliberate choice. Async reducers make testing harder and blur the boundary between pure logic and side effects. The workaround above keeps the separation intact, but has no standard pattern yet.

Open Question

What is the Flow-Arch equivalent of Redux-Thunk? Can a lightweight effect system be designed without middleware complexity, keeping reducers pure and async logic explicit?

experiments/async-actions/ — not started yet
L-04

No Enforced Purity

KNOWN

What happens

Unlike Haskell or Elm, JavaScript cannot enforce that reducers and view functions are truly pure. Side effects can be written inside them with no warning from the language.

JS cannot stop this JS
// This compiles and runs fine — JS does not care
const reducer = (state, action) => {
  console.log("side effect")        // ✗ impure
  localStorage.setItem("x", "y")  // ✗ impure
  return { ...state }               // ✓ pure return
}

// Compare: Haskell makes this impossible at compile time
// IO actions must be declared in the type signature
// pure :: Int -> Int          ← enforced pure
// impure :: Int -> IO Int     ← enforced impure

Honest assessment

This limitation is inherent to JavaScript. It cannot be fully solved without a type system or compiler. Flow-Arch accepts this and relies on discipline, documentation, and code review.

What helps

ESLint rule no-param-reassign catches mutation. TypeScript readonly reduces mutation risk. Neither fully solves the problem — they reduce it.

L-05

String-Based View Has
No Type Safety

KNOWN

What happens

The view function returns an HTML string via a template literal. Typos in HTML structure fail silently. Typos in state properties cause runtime errors. There is no autocomplete, no validation.

silent failure modes JS
const view = (state) => `
  <div class="contaner">       ← typo: silent failure
    <p>${state.usr.name}</p>  ← typo: runtime TypeError
    <p>${state.userInput}</p> ← XSS if not sanitized
  </div>
`

// Minimum protection: sanitize user input
const escape = (str) =>
  String(str)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")

// Always escape before interpolating user data
const view = (state) => `
  <p>${escape(state.userInput)}</p>
`
Open Question

Could a lightweight tagged template literal function provide automatic sanitization and basic validation, without abandoning the pure function view pattern? This could become part of flow-core.js.

L-06

No SSR Implementation Yet

KNOWN

What happens

Flow-Arch components are defined and rendered entirely in the browser. The page is blank until JavaScript runs. Search engines may not see component content.

The platform solution already exists

Declarative Shadow DOM allows Shadow DOM to be rendered as plain HTML by the server — no JavaScript required. Flow-Arch does not use this yet.

declarative shadow dom — server renders this HTML
<!-- Rendered by server. Works without any JavaScript. -->
<flow-counter>
  <template shadowrootmode="open">
    <style>.count { color: #c8f542; }</style>
    <div class="count">0</div>
    <button>+</button>
  </template>
</flow-counter>

<!-- When JS loads, the component hydrates in place -->
<!-- No flash of unstyled content -->
Open Question

How does Flow-Arch's State → View → Reducer loop integrate with server-side rendering and hydration? Can the pure view() function generate both the server HTML and the client render from the same code?

experiments/ssr/ — not started yet
L-07

Component Boilerplate Repetition

KNOWN

What happens

Every Flow-Arch component repeats the same structural boilerplate. The unique logic of each component is buried inside identical scaffolding.

repeated in every component JS
// This exact structure appears in every component
class AnyComponent extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: "open" })
    this.state = createInitialState()
    this.dispatch = (action) => {
      this.state = reducer(this.state, action)
      this.render()
    }
  }
  connectedCallback() { this.render() }
  render() { this.shadowRoot.innerHTML = view(this.state) }
}

// Proposed: a factory that removes the boilerplate
const FlowCounter = createFlowComponent(reducer, view)
customElements.define("flow-counter", FlowCounter)
// This is one of the most promising paths to flow-core.js
Open Question

A createFlowComponent(reducer, view) factory function could eliminate the boilerplate entirely. This would be the first step toward a real flow-core.js. What should its API look like?

Contribute

Found a limitation not listed here?

If you tried something with Flow-Arch and it did not work well, that finding belongs in this document. A limitation honestly reported is a valid and valuable contribution.

Open a GitHub Issue with the label limitation, describe what you tried and what failed. No code required — a clear description is enough.

Open an Issue →