Getting started
uf compared
Against the four things people actually arrive from. The rows uf loses are in the same tables as the rows it wins, because a comparison that only lists strengths tells you nothing you could not have guessed from the home page.
The one-screen version
create-react-app | Vite + React | Next.js | Bun | uf | |
|---|---|---|---|---|---|
| Status | deprecated by React (Feb 2025) | maintained | maintained | maintained | 0.0.0-alpha prereleases |
| Language | TypeScript or JS | TypeScript or JS | TypeScript or JS | TypeScript or JS | Flow |
component / hook / renders / match | Babel preset | Babel preset | Babel preset | no | native |
| Dev server and build | webpack | Vite | Turbopack (or webpack) | bun build | Vite 8 |
| Test runner | Jest | you add one | you add one | bun test | uf test |
| Formatter | you add one | you add one | you add one | none | in the binary |
| Linter | ESLint, wired in | you add one | ESLint, wired in | none | in the binary |
| Type checker | tsc | tsc | tsc | tsc | uf check (Flow) |
| Production server | — | — | next start | Bun.serve, you write it | none yet |
| Config files | none, then all of them after eject | one per tool | one per tool | one per tool | uf.config.js, and only it |
Two rows in that table decide most of the question. If you write TypeScript, the syntax row is worth nothing to you and every other row is a downgrade. If you need the production-server row, uf cannot serve you at all yet.
Read the losses first
| What uf loses | To | By how much |
|---|---|---|
| Test throughput | bun test | 0.20 s against 0.06 s on 50 files, 1,000 tests, 2,000 assertions (Testing) |
| Deployment targets | Next.js | uf build --adapter node writes a directory you can copy, and it is the only one of the seven adapters that exists; edge, serverless and the rest are names (#391) |
| Server Components | Next.js | the split is route-level: a route with no "use client" boundary in it keeps its page out of the client bundle, and every page above a boundary still ships whole (#252, #350) |
| The typed ecosystem | everything | a package with no Flow libdef is any (Flow and TypeScript) |
| Being installable | everything | on npm as prereleases, and only the closure a project needs — @uniflowed/state and @uniflowed/effect are still workspace-only (#210) |
| Runtime coverage | Bun, Next.js | Node.js and Bun load Flow; Deno is a name in an enum and edge runtimes have no host (#246) |
| Being three years old | all four | pre-release, no stability guarantee, interfaces move without warning |
What uf does not do is the complete version of that list, sorted into refusals and gaps.
Against create-react-app
CRA is the honest ancestor: an all-in-one tool that hid a build behind one dependency. React deprecated it in February 2025 and recommends a framework or a build tool instead.
What uf takes from it: the shape. One command, no configuration to copy, a project that runs before you have made a single decision.
What uf takes from its failure: everything in
docs/red-lines.md.
CRA's three failures were an eject cliff with a one-way door, a single
package serialising every upgrade in the ecosystem, and solving the build and
stopping. uf answers the first with a continuum and no eject, the second by
owning orchestration rather than implementation, and the third by being a
framework as well — which is the one of the three uf has not finished.
What CRA still has that uf does not: a decade of tutorials that assume it, and the fact that it runs on stable, published, installable software.
Against Vite + React
This is the closest comparison and the least adversarial one, because uf's
dev server and build are Vite — packages/vite/package.json depends on
vite: "^8.2.2" and uf drives it over a JSON protocol. Bundling performance is
not a row: it is the same bundler.
What uf adds. Flow's React syntax with no Babel anywhere in the pipeline;
one config file instead of a vite.config.ts plus a test config plus a
formatter config plus a linter config; and a test runner, formatter, linter and
type checker that read the same syntax tree the build does.
What plain Vite keeps that uf gives up.
- TypeScript, and with it every typed package on npm.
vite preview. uf has no equivalent; the driver implements one and nothing reaches it (#255).vite/client's types.import.meta.glob,import.meta.hotand asset imports are type errors oranyunderuf check(#264).- Any Vite version you like. uf pins the line it tests against.
What does not change is the escape hatch: the vite key in uf.config.js is
merged over what uf generates, and plugins is a list of ordinary Vite plugins.
Against Next.js
The largest gap, and the one worth being blunt about.
next build produces a running application: next start serves it,
output: 'standalone'
writes a self-contained server.js, and output: 'export' is the opt-in for
the fully static case. In uf, the static case is the only case.
| Capability | Next.js | uf today |
|---|---|---|
| File-system routing, nested layouts | yes | yes (Routing) |
| Data loading before render | yes | loader exports |
| Route handlers | yes | written, dispatched only under uf dev (#250) |
| Server Components, server actions | yes | routes with no client boundary leave the client bundle; nothing above a boundary does, and no server action is callable (#252) |
| Streaming, Suspense boundaries | yes | renderToString, so no (#254) |
| ISR, caching, revalidation | yes | four booleans in a manifest (#277) |
| Middleware | yes | reserved, discovered, never called (#260) |
| Image optimisation | yes, plus a request-time optimizer | build time only: an imported image is resized to the declared widths and gets a real srcset, sizes, intrinsic dimensions and a blur placeholder. No AVIF and no lossy WebP — neither has a pure-Rust encoder uf ships — and no request-time endpoint (#273) |
| Font self-hosting | yes, downloaded and subsetted at build time | an imported font is self-hosted, content-hashed, declared with font-display and paired with a size-adjust fallback computed from its own metrics. Not subsetted, and not downloaded from a provider (#273) |
| Error boundaries | yes | none (#257) |
| Deployment adapters | many | none |
What uf has that Next.js does not. Flow's React syntax, checked by Flow's own inference. A formatter and a linter and a test runner in the same binary, reading the same tree — Next.js is a framework and expects you to bring those. And a framework that does not own a server, which is a feature exactly when you did not want one.
If you are building an application that renders per request, use Next.js. uf will tell you this again on What uf does not do rather than letting you find it out at deploy time.
Against Bun
Bun and uf overlap less than the feature lists suggest: Bun is a runtime that brings a package manager, a bundler and a test runner; uf is a toolchain that runs on a runtime — Bun included.
Where Bun wins, measured. bun test runs the same suite in 0.06 s against
uf test's 0.20 s warm and 0.30 s cold, on 50 files, 1,000 tests and 2,000
assertions, best of five on an 8-core M-series Mac. That is about three times
faster, and the reason is structural: Bun runs everything in one process with
the runner written into the engine, while uf spawns a worker per core, gives
each worker one file at a time, and pays the start-up. bun test also ships
snapshots, coverage and a
JUnit reporter; uf test has none of those
(#280).
Where uf wins the same comparison. uf test is about nine times faster
than vitest run on that suite (1.96 s), it runs on Node.js as well as Bun,
and it runs Flow — bun test has no component or hook syntax at all.
Package management. uf does not implement one: uf install, uf add,
uf remove, uf update and uf why each detect the project's package manager
and run it, refusing a manifest that declares npm scripts and passing
--ignore-scripts to whatever runs. uf install --frozen-lockfile is the CI
install. Bun's installer is a finished product — a resolver, a global cache, a
lockfile format, bun outdated, bun audit, bun patch, catalogs; uf's is a
policy in front of yours, and deliberately: writing a resolver would make uf
the chokepoint every ecosystem upgrade has to pass through.
And Vite+
Vite+ is the toolchain uf will be compared to most often, and it has its own page: uf and Vite+. The short version is that they answer different questions — Vite+ unifies the TypeScript toolchain, uf makes Flow's React syntax usable — and Vite+ has a company and a support contract, which uf does not.
So which one
- Writing TypeScript, shipping to production this quarter: Next.js if you need a server, Vite if you do not. uf is not competing for this.
- Writing TypeScript, unhappy about the number of config files: Vite+.
- Writing Flow, or wanting to: uf, and there is currently nothing else — which is the whole reason it exists.
- Building a static site, curious, willing to be early: uf builds this one.
uf buildprerenders every page of this documentation, and the output is what you are reading.
Where to go next
Migrating to uf is the honest cost of moving from any of the above. Flow and TypeScript is the language decision, which is the one that actually matters.