Another Web Interface Framework
Porting Apache Tapestry To The Browser
React is the most popular web interface development library, unlike more substantial frameworks like Angular and Vue, React focuses just on providing the components for the presentation layer and with only a few key mechanisms to do the job, hence a seemingly low cognitive load — but things tend to get more complicated in reality and the cognative load mushrooms.
Write any relatively sophisticated application with React and you will probably end up debugging it to find out why it will not behave as expected. React also muddles the presentation and data hydration code together which is a pattern going back to PHP. With PHP the barrier to entry is low, and for a small page a bit of code sprinkled through the markup is honestly fine. Java Server Pages (JSP), a Java technology I worked with long ago, also followed the PHP pattern, as Microsoft did later with their Active Server Pages (ASP).
In Java Server Pages the programatic elements are mixed in with the markup, just like PHP, and are encoded as scriptlets — little chunks of Java code. Later JSTL was developed which was much better, however it used custom tag libraries that meant the page was not directly renderable in a browser.
Blending Presentation And Hydration Code An Anti-Pattern
When the markup and the program are blended, it means there cannot be clean separation between the markup design process and the programming process. This creates hand-off friction when changes are made to the markup and the developer has to integrate them back into the page. Practically what tended to happen is programmers ended up both designing and programming web sites, but with a paucity of design skills the resulting sites had a prosaic developer aesthetic. If the design was done by a dedicated designer, delivering site change suffered from added technical impedance.
If the markup and program are separated then a designer can work on the markup and the program can be maintained separately — in theory revisions to the presentation layer and the program can be easily split between the design and development teams. Separation of concerns is also one of the most powerful principles in software development. This realisation was one of the motivations behind Howard Lewis Ships creation of Tapestry, now an Apache project in its 5 generation. These benefits and more are summarised below:
- Parallel work by different roles Designers and front-end people can refine the look and structure while developers implement behaviour. This reduces bottlenecks and hand-off friction.
- Templates remain readable by non-programmers Because the template is mostly HTML, it stays understandable to people who don’t write code. This is rare in frameworks that mix logic and markup heavily (e.g. heavy use of scriptlets, JSX with complex logic, or Angular templates packed with directives).
- Logic becomes easier to test Page and component classes can be unit-tested without rendering HTML. You test behaviour in isolation.
- Clear ownership and lower cognitive load When you open a template, you know you’re looking at structure and presentation. When you open the class, you’re looking at state and behaviour. Mental context-switching is reduced.
- Easier evolution You can redesign the UI without rewriting business logic, and you can change the logic without risking visual breakage.
Separation of concerns is the highest-leverage principle in software architecture: by giving each part of a system one clear reason to change, it makes software dramatically easier to understand, safer to evolve, cheaper to maintain, and possible for teams to scale without collapsing into complexity. React has not followed this path, and is perhaps another example of the narcissistic design underlying many a cargo cult found in the world of software. In contrast, both Vue and more so Angular are similar to Tapestry:
Affinity Ranking: Tapestry → Modern Frameworks
1. Angular — Highest Class-based components with decorators and separate HTML templates. The closest structural and philosophical match.
2. Vue — Medium HTML-based templates with a relatively clean separation of concerns, though its reactivity model differs significantly.
3. React — Lowest JSX mixes markup and logic in the same file. The functional + hooks style sits furthest from Tapestry’s architecture.
React, like PHP and JSP, has perhaps become popular because it looks like doing software development instead of web design — markup is submerged inside functions. Sometimes network effects and ecosystem development can outweigh pure architectural considerations, but the tradeoffs that result can come back to bite.
Project Qloom
So with this dissatisfaction with React in mind and my nostalgia for Tapestry, I though why not use AI to port Tapestry to the browser? Translating code from Java to TypeScript is the kind of thing LLMs can do very quickly, the code is open source and the framework is well documented, so everything looks good — the Hotel Booking web application can also serve as a reference to prove the solution.
Get John Coleman’s stories in your inbox
Join Medium for free to get updates from this writer.
I call this project Qloom for want of a better idea — reuse an already well documented web framework, capture the architectural elegance and produce a light weight alternative to React that I can use for personal projects and tools. Programming will be simple decorated TypeScript — no more hooks of horror! The Hello example looks like this:
import { Page, Property } from "@qloom/runtime";
export class Hello extends Page {
@Property name = "world";
@Property mood = "sunny";
@Property title = "Greetings";
@Property items = ["alpha", "beta", "gamma"];
@Property item = ""; // loop variable — bound by <t:loop value="item">
@Property count = 0;
get hasItems(): boolean {
return this.items.length > 0;
}
onIncrement(): void {
this.count += 1;
}
}<t:panel heading="title">
<h1>Hello, ${name}!</h1>
<p class="greeting ${mood}">You have ${items.length} items.</p>
<t:if test="hasItems">
<t:loop source="items" value="item">
<span class="item">${item} </span>
</t:loop>
</t:if>
<p>
<a href="#" t:type="eventlink" t:event="increment" t:zone="counter" id="bump">Increment</a>
</p>
<t:zone t:id="counter">
<p id="count">Count: ${count}</p>
</t:zone>
</t:panel>Goodbye State Managers
React spawned an entire state-manager industry — Redux, MobX, Zustand, Jotai, Recoil — because component state is ephemeral and local. Anything shared or durable gets lifted into a store, which becomes a second source of truth you have to keep in sync with the URL, the back button, and a page reload. Which is why “why did the back button break?” is its own genre of bug report.
In React, a function component runs again on every render. Anything you want to “remember” must be stored in a hook (useState, useReducer, etc.) or an external store. This creates a permanent need for state containers.
In Qloom, a page or component is a normal TypeScript class instance that stays alive. State is just fields:
@Property username = "";
@Property items: Item[] = [];There is no “how do I keep this value across renders?” problem, so the pressure to reach for Redux/Zustand/Jotai/etc. never appears. Because the framework already answers “where does this state live?”, developers are not forced to invent (or adopt) a general-purpose store.
Qloom’s core invariant is:
A page must be reconstructable from (URL) + (@Persist storage) + (backend API). Never from in-memory state alone.
This rule actively discourages the classic React pattern of putting large amounts of application state in a client-side store that disappears on refresh. If the state matters, it belongs in the URL or in durable storage. If it doesn’t matter, a simple field is enough.
React’s fine-grained reactivity model (and the libraries that grew around it) exists partly because updating the UI efficiently is hard when everything is a function that re-runs.
Qloom updates are deliberate and coarse:
- Change some fields
- Call Zones.refreshZone(“someZone”) (or navigate)
You don’t need a sophisticated reactive graph or selector system to keep performance acceptable. The framework’s Zone reconciler handles the DOM patching.
Tapestry/Qloom comes from a tradition that prefers clear conventions and a relatively opinionated structure. React deliberately left state management as an open problem (“it’s just a UI library”). That vacuum was filled by numerous competing libraries.
Qloom fills the vacuum itself with a small, coherent set of primitives (@Property, @Parameter, @Persist, URL state, Zones). There is much less room — and less need — for third-party state libraries to grow.
Conclusions
I’m not going to pretend this is free. React’s gravity is real — the ecosystem, the hiring pool, the answer for every question. Qloom is a faithful port of Tapestry 5’s model, so if you’ve never met Tapestry, the render-phase protocol and container-based event bubbling are a genuinely new mental model, even as they delete the hook rules and the store. And “no server round-trip” means reconstructability is on you: put navigational state in the URL and durable state in @Persist, or you've re-created the reload bug. There's no free lunch — just a smaller set of costs.
But that’s rather the point. React asks you to internalise JSX, hook rules, dependency arrays, and a state library, then ship a virtual DOM runtime — that’s added complexity. Qloom asks you to write an HTML template and a plain class, keep state where it can be reconstructed, and offload the heavy lifting to the compiler. Less to learn, less to ship, less to get wrong on the back button.
Older ideas, done faithfully, turning out to weigh less than the framework that replaced them — is kind of the whole pitch, right?
Getting your first Qloom application going is trivial:
npm create qloom my-app
cd my-app && npm install && npm run devQloom includes a comprehensive suite of documentation as SKILL.md files that get set up when you create your project so your agentic coding tool, such as Claude Code, can turn your ideas into working web apps with minimal effort.
To read more visit https://github.com/JohnSColeman/qloom.










