Elements vs React Router
React Router is an open-source web app framework for React, made by the team that created Remix. It began as a client-side router, and since version 7 in November 2024 it also includes the Remix v2 framework as "framework mode", so a Remix v2 app upgrades to React Router. In framework mode, you list routes in app/routes.ts or generate them from file names, and each route module exports a React component, a loader that fetches its data on the server, and an action that handles form submissions. It renders pages on the server, in the browser, or ahead of time as static files, and provides cookie sessions, middleware and generated route types. It runs as a Vite plugin, and apps deploy to Node.js or to hosts such as Vercel, Cloudflare and Netlify.
Elements builds the same kind of app. It is an integrated app environment for people and their agents: a project server that runs while you work, a test runner, a bundled Postgres and a deploy command that reaches any Ubuntu server over SSH, with a default framework that just works built into the same system. That framework has a counterpart for React Router's main pieces. Where React Router has loaders, Elements has route handlers that fetch a page's data; where it has actions, Elements has typed server functions that any event handler can call, for reads as well as writes. Pages are Elements HTML instead of JSX, rendered on the server, and the framework also brings sessions, realtime data, background jobs and email. Apps are TypeScript running on Node.js.
A React Router app is a set of parts you put together. Framework mode handles routes, loaders and actions, and around it you add a database client or an ORM such as Drizzle, an auth library, a migration tool, Jest or Vitest, a job queue, an email service, a realtime service, and a Docker image or a host's own build. Each one is chosen, configured and wired by hand, and every joint between two of them is a place the app breaks: a release of one that the others do not support yet, a session cookie that two libraries have to agree on, an error that surfaces in a different tool from the one that caused it. A person loses time chasing those, and an agent goes around in circles. Elements comes ready to build with. Postgres, sessions, migrations, tests, jobs, email, realtime and deploy work together on the first run because they were engineered together, so you and your agent are productive from the first minute. And it stays close to the metal: a page is HTML and CSS, a query is SQL, a server call is a TypeScript function call, and the framework vocabulary on top is short: app.route(), session.login(), sql(), @rpc and tests.
At a Glance
| Elements | React Router | |
|---|---|---|
| What it is | A project server, a default app framework, a database, a test runner and deploy as one coherent system | A React router with a framework mode, run as a Vite plugin |
| Templates | Elements HTML: HTML tags, with TypeScript in {} |
React components in JSX |
| Routing | Declared in index.ts with web standard URLPattern |
Declared in app/routes.ts, or generated from file names |
| Loading data | The route handler queries Postgres and passes the rows to the page | A loader per route, with a database client you choose |
| Server calls | @rpc: typed function calls from the page, for reads and writes |
An action per route; afterwards every loader on the page runs again |
| Sign-in and sessions | Sessions built in, the same over HTTP and WebSockets | Cookie session storage; sign-in and password checks are yours to write |
| Database | Postgres, bundled, with sql() built in |
Bring your own database and client |
| Migrations | Applied when you save them | Not included |
| Build errors | One build: type errors, failing tests, migration errors and server code reachable from the browser | Separate steps: react-router typegen && tsc, plus a test runner you add |
| Tests | Part of the build | Not included; the docs offer a stub helper for unit tests and point to Playwright or Cypress for full routes |
| Realtime | LiveTable and channels | Not included |
| Background jobs and cron | Built in | Not included |
| Built in, with templates in Elements HTML | Not included | |
| Deploy | elements deploy to any Ubuntu server over SSH, usually under a second |
Docker templates for Node, or a host's own setup for Vercel, Cloudflare or Netlify |
Agents Need Standards, Not a React Ecosystem
Much of the case for React Router rests on React: a large catalog of open-source components and libraries. Each one is another package, with its own setup, its own API and its own seam with the rest of the app.
For an agent, a React package earns its place only when it does something the agent cannot write quickly from HTML, CSS and TypeScript, and that list is short. An agent has read far more plain HTML, CSS, SQL and TypeScript than any component library's docs. A modal is a <dialog> and some CSS, done before you would have finished comparing modal packages, and nothing about it ever needs a version bump. Each package past that short list is one more API to read and one more seam to break. The packages that do real work stay: Stripe, the AWS SDK and the rest of npm install into Elements unchanged.
None of this argues against frameworks. An agent does not write machine code to build a web app, nor does it want a package to subtract two numbers; the right amount of framework sits between, at the plumbing every app needs: routing, typed server calls, sessions and a database driver. The Elements app framework ships that plumbing as app.route(), @rpc, session.login() and sql(). Past it, returns diminish: a further layer of hooks, providers and conventions costs an agent more to learn and get right than it saves. What pays off there is correction, learning that a change broke something the moment it breaks, so that is where Elements puts its engineering, and everything past the plumbing stays HTML, CSS and TypeScript function calls. One coherent system checks every route, template, query, test and migration the agent writes, finding every build error, from type checking, tests, migrations and program analysis, in milliseconds. The project server that runs while you work holds the build graph and build state in memory, so on every save the build state is updated almost instantly, and elements build -json answers agents and humans in microseconds (build). Your editor, your terminal and every agent read that one build, and server code needs no async or await, so there is no await for an agent to leave out.
Templates Are HTML, Not JSX
A React Router page is a component in JSX that reads its data through loaderData or hooks such as useLoaderData and useFetcher. An Elements page is Elements HTML: ordinary tags, TypeScript between braces, and e:if and e:for attributes where JSX would use && and .map().
<ul>
<li e:for={comment of comments}>
<strong>{comment.author}</strong>: {comment.text}
</li>
</ul>
<p e:if={comments.length === 0}>No comments yet.</p>
The server sends this as finished HTML, and once it is live in the browser, a new comment adds one <li> rather than re-rendering a component tree, because the runtime tracks which markup reads which data. Inputs are two-way bound without a useState and onChange pair: typing changes the data, and setting the data changes the input.
React adds hooks, with rules for where they may be called, and memo, useMemo and useCallback for skipping re-renders. Elements has no re-render to tune: the runtime patches the DOM at the smallest level each change requires (Elements HTML).
The Database Is Part of the Framework
A web app needs a database. React Router has no data layer: a loader or action calls whatever database client you install. The Remix team says so itself: its Remix 3 announcement lists "punting on the database" among what Remix 3 leaves behind, adding "no offense, React Router." React Router's Postgres template adds the Drizzle ORM and a custom Express server to fill the gap (deploying).
Elements does not punt. Postgres comes with it, installed and configured locally and on each deploy target, with no Express server or ORM to add. The code that would have been a loader runs plain SQL through the sql() function, and the compiler binds each ${value} as a parameter so it cannot be read as SQL.
The schema lives in migrations, SQL files that apply the moment you save. Change one during development and Elements synchronizes the change to the database. When you deploy, whatever has not run yet runs once, in one transaction, and if it fails nothing ships.
Server Functions Are Ordinary Typed Calls
In React Router, a <Form> or a fetcher sends a write to the route's action, and the result comes back to the component through actionData. Reads go through the route's loader. Both are tied to a route and a URL.
Elements unties them. Add @rpc to a function and any event handler can call it, for a read or a write, with no route to declare. Arguments and the result are typed end to end, and the result can be live data that keeps the page current. The build turns the call into a network request, and the body stays on the server.
That boundary is enforced per call. If browser code reaches for sql, tx or session.login directly, compilation stops with "Security error: cannot call server code from the browser without going through an rpc function."
A Change Reaches Every Browser Watching the Page
When a user saves something, the page has to show it. In React Router, a form or fetcher calls the route's action. When it finishes, "all loader data on the page is revalidated": every loader on the page runs again. That refresh reaches only the browser that submitted. Everyone else viewing the same page sees the change when they next navigate or reload, unless the app adds a realtime service of its own.
Elements sends the change to everyone watching. A LiveTable holds the rows a page shows and keeps them in sync across browsers. When a user adds a comment, it appears on their screen at once while the server confirms it, and every other browser on the page patches in that one row, with no loaders to re-run. Messages that are not rows go over channels, from the server to whoever is subscribed. Postgres carries both, so there is no separate realtime service to run.
import { LiveView, session } from "@elements/app";
import type { Comment } from "#app/shared/services/comments";
function onSubmit(comments: LiveView<Comment>, form: { text: string }) {
comments.insert(
{ text: form.text, author: session.get("userName")!, createdAt: new Date() },
() => form.text = "",
);
}
<html (comments: LiveView<Comment>, private form: { text: string } = { text: "" })>
<ul>
<li e:for={comment of comments}>
<strong>{comment.author}</strong>: {comment.text}
</li>
</ul>
<form onsubmit={() => onSubmit(comments, form)}>
<input value={form.text}>
<button>Post</button>
</form>
</html>
The route that serves this page reads the rows with comments.view(), so the page renders on the server with them already in it.
Sessions Are Built In, All the Way to the Template
React Router provides session storage: helpers that read and write a signed session cookie, or store the session wherever you implement it. The users table, the password hashing and the sign-in check are yours to write: the docs' login example calls a validateCredentials function it leaves to you.
Elements sessions go past the cookie. The session a route handler sees is the same one a server function or a template sees, whether the request came over HTTP or a WebSocket, and a header that shows the user's name changes on sign-in with no reload or revalidation. Sign-in is a short server function that checks a password hash inside Postgres and calls session.login(), and the authentication recipe walks through the flow end to end.
Every Build Error Comes From One Build
An agent learns what its change broke from build errors. In React Router, tests and type checking are separate tools, and migrations are not part of it at all. Its testing guide offers createRoutesStub for unit testing components with a runner you install, such as Jest or Vitest, and points to Playwright or Cypress for testing full routes. Type checking is a separate command: the default template's typecheck script runs react-router typegen && tsc.
Elements runs tests, migrations, type checking and program analysis, such as server code reachable from the browser, as steps of one build that finishes in milliseconds, and any failure fails the build. After an edit it runs the tests that edit can affect, each wrapped in a Postgres transaction that is rolled back at the end, and no fixture teardown is needed. Route types need no generating, and elements build -json returns every build error across code and templates, migration failures and test results in one JSON response.
Deploys Usually Finish in Under a Second
React Router's deploy guide offers templates that build a Docker image for Node.js, to run on a container service you choose, and points to hosts such as Vercel, Cloudflare and Netlify for their own setups. Either way, tests and migrations are steps you add to your own pipeline.
Elements skips the image and the container service. elements deploy goes over SSH to an Ubuntu server from whichever provider you like, and rather than pushing an image, the project server on your laptop compares notes with the one on the server and transfers only changed files. A new machine is ready in a few seconds; after that, a deploy usually takes under a second. The steps you would have added to a pipeline are built in: every machine runs the tests, the first machine runs migrations once, and a failure anywhere halts the deploy with the old release still up. The load balancer, TLS certificates and Postgres are configured on the server as part of it. Use that Postgres for a prototype or one server; for several servers, or for managed backups and scaling, set DB_HOST to a hosted Postgres such as DigitalOcean Managed PostgreSQL or Amazon RDS (setup).
Who Pays for React Router
The team behind React Router joined Shopify, a commerce company, in 2022. The announcement said, "Under Shopify's stewardship Remix receives long-term backing and support from an established leader in commerce," and "Shopify itself will use Remix across many projects" (Remixing Shopify). React Router is the on-ramp to Shopify's commerce platform: Hydrogen, "Shopify's stack for headless commerce," "is designed to dovetail with React Router."
Elements is a company that sells its tooling directly, per machine, with nothing metered (pricing), so what it earns from is the tooling itself. The framework is not the product: the app framework and runtime packages such as @elements/app are MIT licensed, and the app you build on them is yours (licenses).
Moving From React Router
An agent can port a React Router app, or a Remix v2 app, easily, because each route module maps onto the Elements app framework's routes, HTML templates and @rpc functions. You keep TypeScript and your npm packages, and you gain sessions, Postgres with migrations, tests and deploy without adding a library for each.
Questions
What happened to Remix?
In November 2024, React Router v7 took in the Remix framework as its framework mode, so a Remix v2 app upgrades to React Router (Wake up, Remix!). The Remix name now belongs to Remix 3, a separate framework from the same team that does not depend on React.
Is Elements a React Router alternative?
Yes. It replaces React Router's framework mode, which is what Remix v2 became, together with the database client, auth code, test setup and Docker image an app adds around it.
What is the difference between Elements and React Router?
React Router is an open-source React framework that handles routing, data loading, form actions and rendering, and leaves the database, sign-in, tests, migrations, jobs and realtime to other libraries and services. Elements is an integrated app environment. Route handlers stand in for loaders and @rpc functions for actions, and sign-in, realtime, jobs, email, Postgres with migrations, tests and deploy come in the same system. Type errors, test failures and migration errors come back from one build.
Can I use React components in Elements?
No. Pages are Elements HTML, ordinary markup with typed expressions, server rendered and patched in place in the browser. An agent rebuilds a React component as an Elements HTML template easily, and the result has no hooks or memoization to manage.
Can I use npm packages in Elements?
Yes, unmodified, into a normal node_modules directory. You do not run a separate install; the build does it.
How do I move a React Router or Remix v2 app to Elements?
An agent can port it easily, because each route module maps onto the Elements app framework's routes, HTML templates and @rpc functions. You keep TypeScript and your npm packages, and you gain sessions, Postgres with migrations, tests and deploy without adding a library for each.