Not All Abstractions Are Created Equal
Solid 2.0 hit RC in August and is on its eleventh RC as I write this. The discussion around it is signals versus virtual DOM, plus a benchmark table. I have sat through a lot of framework arguments on real teams, and a benchmark table has never decided one. The questions that decide them are quieter. Where does shared state live? Can I reuse this logic on the next screen without ceremony? Are we building web pages here, or are we building an application that fifty engineers will still be extending in five years, long after everyone who made the framework choice has changed jobs?
Frameworks price those questions very differently, and the pricing is what this post is about. Not all abstractions are created equal, and they are not priced for equal splits.
The same table, built twice
To have something concrete to argue from, I built the same page twice: an inventory table over a faker-backed API with 400 to 1200ms of injected latency, a category filter, a refresh button. Once on React 19, once on the Solid 2.0 RC. Same API module, same stylesheet. Both apps are at same-table-built-twice.
The React page came out at 81 lines plus the nine-line mount file, and the Solid page at 56 with no mount file at all, and the missing lines are specific lines. The React version owns five pieces of state: the category, a refresh counter, the rows (with null meaning never loaded), a pending flag, an error slot. It also owns a cancellation guard in useEffect, and that guard is load-bearing. Without it, a slow response for the old category lands after a fast response for the new one and silently overwrites it. It's the kind of bug that ships quietly and gets blamed on the API for a year. The Solid version has two signals and one async memo; loading, staleness, and error handling come from the runtime.
On a page this size, who cares. Thirty-four lines is nothing, any mid-level engineer writes that useEffect correctly from muscle memory, and if they don't, TanStack Query exists. For a marketing site, a dashboard, a form over a database, the difference between these two files will never show up in your delivery dates. This is the first half of the point: at a small enough split, the abstractions are interchangeable, so pick the one your team already knows and stop reading framework posts.
The second half is what happens when the app stops being one page.
The singleton test
Here is a test I apply to any UI framework: show me your singleton. Every application above a certain size has state that is one-per-app: the session, the feature flags, the permissions set, the cart, or in an inventory system, the stock levels that a dozen screens read and two mutate.
In Solid, the singleton is a module:
// stock.js
import { createSignal } from 'solid-js';
export const [stock, setStock] = createSignal([]);
Import stock anywhere and read it. Every component, memo, or effect that reads it updates when it changes. There is no provider to mount, no tree position to negotiate, no library to adopt. The language's own module system, which already deduplicates and already handles circular imports, is the state architecture.
React makes you choose. Context means mounting a provider and accepting that every consumer re-renders on every change, so above a modest size you start splitting contexts and memoizing children defensively. Or you adopt an external store, and now Redux or Zustand conventions are part of your onboarding docs. None of these are bad options. They are decisions, with migration costs when you pick wrong, and an application accumulates dozens of them. In one framework the decision is "make a module." In the other it's a design review.
Functions that compose, and functions that don't
The deeper version of the same difference is what reusable logic looks like.
React's unit of reuse is the hook, and hooks come with rules. Call them at the top level, never conditionally, only from components or other hooks. The rules exist for a real reason, and they also mean a hook is not a function in the ordinary sense. You cannot call useInventory() from a plain TypeScript module, a worker, a test without a harness, or an if branch. The logic is fused to the render tree. Testing it means renderHook and a fake component tree, a small theater production you stage so one function will agree to run. Which is why data logic on large React teams migrates out of hooks and into external stores, where it turns back into functions.
Solid's primitives are already functions. createSignal and createMemo compose by calling each other, in any order, behind any conditional, in any module. A derived value like
export const lowStock = createMemo(() =>
stock().filter(item => item.qty < 10)
);
lives next to the singleton it derives from, in the same plain module, and any screen that imports it gets a live, cached view. Business logic stays in modules that look like business logic. The component layer imports it instead of hosting it.
This is the property I actually care about, more than reactivity models or rendering strategy: whether the framework lets domain logic live outside the UI without changing shape. At a 90/10 split of pages to shared logic, it barely matters. At 40/60, it's most of the architecture.
So: simple pages or an enterprise app?
If what you're building is pages (a site, a portfolio, a small tool, even a modest SaaS front end) the honest advice is that framework choice is a hiring and momentum question, and React wins those by default. The constraints I've described are real and you will not feel them. Anyone who tells you your landing page needs fine-grained reactivity is selling something.
The enterprise case is where the pricing diverges. Big apps are mostly shared logic wearing many screens, the singleton count grows every quarter, and hook-shaped logic compounds into the ceremony Solid's model deletes. On the merits of the abstractions, 2.0 is the strongest argument Solid has ever had for that workload, and the async model closing the null-check gap removed the last excuse I had for not taking it seriously.
And yet I would still hesitate to bet a five-year, fifty-engineer application on it this quarter, for reasons that have nothing to do with abstractions. It's an RC; eleven of them have shipped since August, and rc.8 moved the diagnostics surface to a different export mid-cycle, flagged breaking. The ecosystem shelf is thinner: fewer component libraries, fewer people on the job market who know it, fewer answers already written when something breaks at 2am. Enterprise choices are risk portfolios, and maturity is a line item in them whether I like it or not. The abstractions argue for Solid; the actuarial table still argues for React. Watch whether that changes as 2.0 settles, because the technical half of the ledger already flipped.
There is a systems story inside 2.0 that I cut from this post: it made a deliberate consistency-versus-availability trade, keeping stale rows on screen during refetches instead of tearing down to spinners, and the RC changelogs read like database postmortems. It's good work. It's also not what should drive your decision, which is why it's a paragraph now.
The split is the decision
The framework question was never really React versus Solid. It's a question about your application: what fraction of it is shared logic, singletons, and domain rules, versus pages that render and move on. Measure that split honestly, price the abstractions against it, and most framework arguments end early. A 90/10 app should optimize for familiarity and hiring. A 40/60 app should look hard at where its logic is allowed to live, because it will pay the reuse tax on every screen, every quarter, for the life of the system, and systems like that have a way of outliving their own obituaries.
One command scaffolded each of these demos. The toolchains have owners now, the compilers are native, and the tooling argument that kept backend engineers dismissing frontend is mostly gone. What's left is the old, unglamorous architecture question, the same one you'd ask about any codebase: where does the logic live, and what does it cost to move.