Frontend Frameworks

Choosing the right frontend framework matters. Each has real trade-offs, and "more powerful" doesn't mean "better for your project."

LLM-Friendliness Scores

We evaluated each framework for how easily an LLM can understand, navigate, and modify the code:

DimensionUIxRe-frameFulcro
Conceptual Understanding544
Code Navigation544
State Inspection553
Modification Task543
Debugging544
Framework Idioms544
Weighted Total5.0/5.04.15/5.03.60/5.0
LLM-Friendliness9.5/108.3/106.5/10

UIx - Simple, Explicit, Fast to Build

Choose UIx when:

  • You’re building an MVP or prototype
  • Your data is mostly flat (lists, forms, simple state)
  • You want the fastest LLM-assisted development
  • Your team knows React hooks
  • Entities don’t repeat across multiple UI locations

Real-world examples where UIx wins:

  • Todo apps, note-taking apps
  • Landing pages with interactive elements
  • Simple dashboards with independent widgets
  • CRUD interfaces for single entities
  • Developer tools and utilities

Why it works: UIx is closest to raw React. State lives in atoms, components subscribe explicitly with use-atom. No magic, no indirection. An LLM can trace data flow in seconds.

;; UIx: State is just an atom, subscription is explicit
(let [state (uix/use-atom app-state)
      todos (:todos state)]
  ($ :ul (for [todo todos] ...)))

Re-frame - Structured, Scalable, Team-Friendly

Choose Re-frame when:

  • You need explicit patterns your team can follow
  • You want excellent debugging tools (re-frame-10x)
  • Your app has complex event chains (async flows, side effects)
  • You’re building a medium-to-large SPA
  • You value the large community and documentation

Real-world examples where Re-frame wins:

  • Multi-page SPAs with complex routing
  • Apps with significant async operations (API calls, WebSockets)
  • Projects where multiple developers need consistent patterns
  • Apps requiring time-travel debugging
  • When you want enforced unidirectional data flow

Why it works: Re-frame’s event/subscription pattern creates guardrails. Every state change goes through an event handler. Every read goes through a subscription. This makes debugging deterministic and patterns consistent across a team.

;; Re-frame: Explicit event dispatch, subscription-based reads
(rf/dispatch [:add-todo "Buy milk"])
(let [todos @(rf/subscribe [:todos])] ...)

Fulcro - Normalized Data, Automatic Consistency

Choose Fulcro when:

  • The same entity appears in multiple UI locations
  • You have deeply nested or graph-like data
  • You need optimistic updates with rollback capability
  • You want server-driven UI with Pathom
  • Data consistency across components is critical

Real-world examples where Fulcro wins:

  • Social apps: A user’s name/avatar shown in header, comments, profile, and friend lists - update once, reflects everywhere
  • E-commerce: Product appears in catalog, cart, recommendations, and order history
  • Admin panels: Entity relationships (users -> orders -> products) with cross-referencing
  • Collaborative tools: Real-time updates where multiple users edit shared data
  • CRMs: Contact shown in multiple contexts (deals, emails, meetings, notes)

Why it works: Fulcro’s normalized database stores each entity exactly once. Components hold references (idents), not copies. When data changes, every component showing that entity updates automatically.

;; Fulcro: Normalized state - Alice exists in ONE place
{:person/id {123 {:person/id 123 :person/name "Alice" :person/avatar "..."}
             456 {:person/id 456 :person/name "Bob"}}
 :root/current-user [:person/id 123]      ; Reference, not copy
 :root/friends [[:person/id 456]]}        ; References, not copies

;; Update Alice's name once -> every component showing Alice updates

The Data Duplication Problem

Consider an app where a user appears in 3 places:

  • Header: “Welcome, Alice”
  • Sidebar: Alice’s avatar
  • Profile page: Full details

In UIx/Re-frame (flat state):

;; Alice's data might be duplicated
{:current-user {:name "Alice" :avatar "..."}
 :sidebar {:user {:name "Alice" :avatar "..."}}
 :profile {:user {:name "Alice" :avatar "..." :email "..."}}}

;; When Alice updates her name, you must update ALL locations
;; Or refetch everything
;; Or build your own normalization layer

In Fulcro (normalized state):

;; Alice exists exactly once
{:person/id {123 {:person/id 123 :person/name "Alice" :person/avatar "..." :person/email "..."}}
 :root/current-user [:person/id 123]   ; Just a pointer
 :sidebar/user [:person/id 123]        ; Same pointer
 :profile/user [:person/id 123]}       ; Same pointer

;; Update once -> UI updates everywhere automatically

When Fulcro is Overkill

Fulcro’s normalization is pure overhead when:

  • Entities don’t repeat across the UI (most simple apps)
  • Data is flat (todo list, form wizard)
  • You’re prototyping and speed matters more than architecture
  • Your team doesn’t already know Fulcro

For a todo app, Fulcro adds ~3x complexity for zero benefit. The normalized database, idents, queries, and mutations are all cognitive tax with no payoff.

Decision Matrix

Your SituationFrameworkWhy
Building an MVPUIxFastest to iterate, lowest complexity
Todo app, notes app, simple CRUDUIxFlat data, no entity duplication
Team needs consistent patternsRe-frameEnforced architecture, great tooling
Complex async flowsRe-frameEvent chains, effect handlers
Same entity in multiple UI locationsFulcroAutomatic consistency
Admin panel with entity relationshipsFulcroGraph data, cross-references
E-commerce (product in cart + listing + recs)FulcroNo duplicate product data
Real-time collaborative editingFulcroOptimistic updates, rollback
API-only backendnilNo frontend needed

Files to Touch Per Feature

Adding a typical feature (e.g., “Clear Completed” button):

FrameworkFilesNotes
UIx4state.cljc, server, ws/client, ui component
Re-frame5state.cljc, server, ws/client, events.cljs, ui component
Fulcro2-3mutations.cljs, ui component (+ optional ws)

The LLM Trade-off

For LLM-assisted development, complexity is a tax on every interaction.

With Fulcro, the LLM must:

  • Understand normalized vs denormalized state
  • Know which table to update
  • Remember to update both table AND references
  • Understand query composition with comp/get-query
  • Handle ident resolution

This context overhead eats into the LLM’s effective capability. For simple apps, you’re paying this tax for nothing.

Bottom line: Fulcro’s power is real, but it’s power you only need for apps with significant data relationship complexity. For most apps, UIx gives you 90% of the capability with 20% of the cognitive load.