Elements vs React
React is an open-source JavaScript library that describes itself as "the library for web and native user interfaces". A React app is a tree of components: functions that take props and return JSX. A component keeps state with hooks such as useState, useReducer and useContext, and reaches outside React with useEffect. When state changes, React calls the component function again, along with the components nested inside it, compares the new output with the last (the approach commonly called a virtual DOM), and applies the difference to the DOM (render and commit). React DOM renders to the browser, and React Native renders to mobile platforms. Since React 19, Server Components render on the server ahead of time and Server Functions let browser code call the server, both implemented by a framework or bundler (Server Components). React was created at Facebook. Since February 2026 it has been owned by the React Foundation, hosted by the Linux Foundation (the React Foundation).
React is a view layer. It renders UI and leaves routing, server data, sessions and the server itself to other code, and its docs recommend starting a new app with a framework, listing Next.js and React Router first (creating a React app). Elements plays in that category with its own view layer. Elements is an integrated app environment, built for people and their agents: a project server that runs while you work, a package installer, a test runner, a bundled Postgres database and a deploy command, shipped as one coherent system with a default framework that just works. Pages in that framework are written in Elements HTML, a language extension to HTML plus a runtime that runs in the browser. An .ehtml file is standard HTML with TypeScript expressions in {...}, typed templates, and a few e: attributes such as e:if, e:for and e:switch. The Elements compiler type checks it with the rest of the app and emits JavaScript, and the Elements runtime ships to the browser beside it. Each page arrives as complete HTML, and the same page is fully reactive in the browser: event handlers run there, inputs bind two ways, and the runtime patches the DOM in place when data changes. Elements apps are written in TypeScript and run on Node.js.
A React app starts with a library and a list of decisions. Pick a framework or assemble one from a router, a state library, a data fetching cache and a bundler that runs Server Components, and then add the database, auth, migrations, tests, jobs, email, realtime and deploy that every app needs past the view. Each pick has its own API and its own docs, and every seam between two of them is a place the app breaks, for a person or for an agent. Elements is ready the moment you run elements create: one well engineered system, so you and your agent build productively from the first minute. It stays close to the metal: a page is HTML and CSS, state is a plain object, 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 | |
|---|---|---|
| What it is | A project server, a default app framework, Postgres, tests and deploy, shipped as one coherent system | A UI library; its docs recommend adding a framework |
| Pages | Elements HTML: standard HTML with TypeScript in {}, compiled and type checked by the build |
Components written in JSX |
| Server rendering | Every page, always: the first response is complete HTML | Through a framework, or React DOM's server APIs wired up by hand |
| In the browser | The Elements runtime attaches to the server-rendered page; handlers run there and inputs bind two ways | React hydrates the page, or renders it in the browser |
| When data changes | The runtime patches the bindings that read it | The component and the components nested in it render again, then React applies the difference |
| State | Plain objects and arrays: change a field and the page updates | Read-only values replaced through a setter |
| Shared state | Objects passed as attributes stay reactive across templates, and the session is reactive everywhere | Context with a reducer, or a state library |
| Render tuning | None to do | memo, useMemo and useCallback, or the opt-in React Compiler |
| Data from the server | A route handler renders the page with its data; @rpc calls and LiveTable rows return data |
Your framework's loader, or a client cache such as TanStack Query or SWR |
| After the first response | The server sends data only, never HTML | With Server Components, the server sends rendered output, the RSC Payload, on later navigations |
| Server code | An @rpc function; browser code that calls sql() is a compile error |
Server Components and Server Functions, split by "use client" and "use server" |
| Realtime | LiveTable and channels | Not included |
| Build errors | One build: type checking, tests, migrations and program analysis, found in milliseconds | Not included; the Rules of Hooks are checked by a separate ESLint plugin |
| Past the view | Routing, sessions, Postgres, migrations, tests, jobs, email, caching and deploy | Not included |
Elements HTML Renders on the Server and Runs in the Browser
Every web UI has to answer two questions: what the first response contains, and what happens in the browser after it. React's answer depends on the framework around it. Elements HTML answers both the same way on every page.
The first response is complete HTML. A route handler passes the page its data, Elements renders the template on the server, and the browser paints the finished page with its content in it. Search engines index the content, and visitors see it on first paint.
The page is reactive in the browser. The Elements runtime attaches to that HTML. Event handlers run in the browser, value and checked bind inputs two ways, and when data changes the runtime patches the DOM in place, at the smallest level each change requires (reactivity).
The server returns data, not UI. After a page's first response, the server never renders an update and never sends HTML. An @rpc call returns a typed value, a channel delivers a message, and a LiveTable delivers rows, and in each case the browser renders the data with the page's own templates.
Elements' LiveView type is unrelated to Phoenix LiveView. A LiveView is one request's rows from a LiveTable: data held in the browser and kept in sync with the server as row changes. The rows are in the page's first response, and after that the server sends only row changes, which the browser renders.
A template is compiled, not interpreted. The Elements compiler reads each .ehtml file with the rest of the app, so a misspelled field in a template fails the build like a misspelled field in a function, and rename and go-to-definition work across templates and TypeScript alike (attributes). The markup is checked once and becomes both the code that renders the page on the server and the code that runs it in the browser. The syntax itself is HTML, and Elements vs JSX compares the two languages line by line.
React Re-renders Components; Elements Patches What Changed
When state changes, a UI library has to work out what on the screen depends on it. React finds out by running code again. Its docs explain that React calls the component whose state changed, then each component it returns, and so on down the tree, and they warn: "The default behavior of rendering all components nested within the updated component is not optimal for performance if the updated component is very high in the tree" (render and commit).
The fixes are more API. React offers memo to skip a component's render, useMemo to keep a computed value, and useCallback to keep a function's identity so a memoized child does not render again. The docs for the React Compiler, which automates this, call manual memoization "tedious, easy to get wrong," and the compiler is "an optional addition to React today" that you install and configure (React Compiler).
Elements does not re-run a page to find out what changed. The runtime knows which markup reads which data, so changing todo.done repaints the bindings that read todo.done, and a new row in a list is one inserted element. Lists reconcile rows by id, a column every generated migration gives a table. There is no render to skip, so there is nothing to memoize and no compiler to opt into.
State Is Plain Data You Change
Every interactive page holds state: the items in a list, what is typed in a form, which panel is open. React's docs say to "treat any JavaScript object that you put into state as read-only." Changing a field directly does nothing on screen, because "without using the state setting function, React has no idea that object has changed" (updating objects in state). An update builds a new object or array and passes it to the setter. The docs suggest a further library, Immer, for code that reads like a mutation.
In React, marking one item in a list done replaces the array:
app/todos.tsxReactfunction toggle(id: string) { setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)); }
In Elements, it changes the field:
app/pages/todos/template.ehtmlElementsexport interface Todo { id: string; title: string; done: boolean; } function toggle(todo: Todo) { todo.done = !todo.done; } <html (todos: Todo[])> <ul> <li e:for={todo of todos} class={{ done: todo.done }}> <button onclick={() => toggle(todo)}>{todo.title}</button> </li> </ul> <p>{todos.filter(todo => !todo.done).length} left</p> </html>
The class on that one <li> and the count below the list update, and nothing else is touched. Objects and arrays pass by reference, so a handler can change them wherever they came from, and a child template that changes an attribute it received updates the parent with no callback passed down (reactivity).
Hooks bring rules of their own. React's Rules of Hooks forbid calling a hook inside a condition, a loop, an event handler, or a try/catch block, or after an early return, and a separate ESLint plugin checks them. A handler that reads state sees the values from the render that created it, a behavior with an issue open on React's own tracker since 2019: "Design decision: why do we need the stale closure problem in the first place?", with 171 reactions as of October 2026. An Elements handler is a named TypeScript function that receives the objects it works on, so it reads their current values, and none of those rules exist.
State Shared Across a Page Needs No Providers
Most apps share some state between distant parts of a page: the signed-in user, a cart, a selected item. React's recipe for "a complex screen" is two contexts, one for the state and one for a dispatch function, filled from useReducer and provided to every component below it (scaling up with reducer and context). Past that, apps add a state library such as Redux or Zustand, each with its own store, API and docs.
In Elements, shared state is an object passed as an attribute, and it stays reactive in every template that reads it. The session is shared already: the same session is read in a route, an @rpc function and a template, and a header that reads it updates the moment the user signs in or out. Rows that several pages and several users share live in a LiveTable, kept in sync by the server.
Server Data Arrives as Data, and the Browser Renders It
Most pages show data from a database, and a UI library has to get it from the server and keep it current. React's docs list the problems with fetching in an Effect: "Effects don't run on the server," so the server-rendered HTML holds only a loading state; fetches form "network waterfalls"; data is usually neither preloaded nor cached; and it takes "quite a bit of boilerplate" to avoid race conditions. Their advice is to use a framework's data fetching or "a client-side cache," naming TanStack Query, useSWR and React Router (synchronizing with Effects). That is one more library, with its own cache keys and its own invalidation after every write.
Server Components take a different path. They render on the server into the RSC Payload, which Next.js's docs define as "a compact, serialized representation of the rendered React Server Components tree," and on later navigations "the RSC Payload is prefetched and cached" (server and client components). In that model the server keeps sending rendered UI after the first page load, and the page is split into components that can hold state and components that cannot.
Elements keeps one rule. The route handler renders the page with its data in the first response. After that, the server sends data and the browser renders it. Here a search form calls an @rpc function, and the list renders the rows it returns:
app/pages/search/template.ehtmlElementsimport { sql } from "@elements/app"; export interface Product { id: string; name: string; price: number; } /** @rpc */ export function search(text: string): Product[] { return sql<Product>(` select id, name, price from products where name ilike ${"%" + text + "%"} order by name limit 20 `).all(); } <html ( products: Product[], private text: string = "", )> <form onsubmit={() => products = search(text)}> <input type="search" value={text}> <button>Search</button> </form> <ul> <li e:for={product of products}>{product.name}: {product.price}</li> </ul> </html>
The route calls the same search("") so the first response already lists products. In the browser, the call to search becomes a network request, its body and its SQL stay on the server, and the result comes back typed as Product[]. There is no await to write, because the compiler adds it (sync-style async), and no client cache to invalidate. For rows that change while the page is open, a LiveTable applies the user's own inserts, updates and deletes optimistically and patches every other watching browser when the same rows change.
The Compiler Draws the Server Boundary
An app that runs code on the server and in the browser needs a line between the two, so that queries and secrets stay on the server. With Server Components, React draws it by file. A file that begins with "use client" starts a client boundary, and "all of its imports and the components it directly renders are included in the client bundle." Server Components cannot use state or context, so a context provider goes in a client file of its own, and props that cross the boundary "need to be serializable" (server and client components). Server Functions are marked with "use server" (Server Functions).
In Elements, one .ehtml file holds the page, its handlers and its server functions, as in the search example above, with no directive. The function marked @rpc runs on the server. The template renders on the server for the first response, and the template and its handlers run in the browser after it. The compiler enforces the line per call: browser code that calls sql(), tx(), session.login() or email directly fails the build with "Security error: cannot call server code from the browser without going through an rpc function" (rpc).
The React Ecosystem Is Libraries; Agents Need Standards and Fast Correction
Much of the case for React is its ecosystem: more components, hooks and libraries than any other UI library, and a large community using them. More app code is now written by AI agents such as Claude Code, Codex and Cursor, and an agent counts a library differently. A library saves an agent work only when it does something the agent cannot write quickly from HTML, CSS and TypeScript, and that list is short. A date field is an <input type="date">, a modal is a <dialog>, and a dropdown is a <select> or a few lines of CSS. An agent writes those faster than it can compare five React packages, read the one it picked and adapt its props, and the result has no version to keep in step with React. The packages that do real work, such as Stripe, the AWS SDK and the rest of npm, install into an Elements app unchanged.
Each React package past that short list is another API and another seam. React's own docs give an example: the error "Hooks can only be called inside the body of a function component" has three common causes, and one is "more than one copy of React in the same app," which happens when "a library you're using incorrectly specifies react as a dependency" (invalid hook call). The issue asking React to report that cause clearly, "Hooks + multiple instances of React", has been open since 2018 and, as of October 2026, has 383 reactions and more than 500 comments.
None of this argues against frameworks. An agent does not write machine code to build a web app, and it does not want a package to subtract two numbers. The right amount of framework sits between those ends, 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, and hooks, providers, memoization and state stores each cost an agent more to learn and get right than they save. What pays off there is correction: learning what a change broke the moment it breaks. Elements keeps everything past the plumbing in standards an agent already knows, HTML, CSS, SQL and TypeScript, and puts its engineering into correction. One coherent system checks every template, function, query, test and migration, so every build error, from type checking, tests, migrations and program analysis, is found in milliseconds. The project server holds the build graph and build state in memory, on every save the build state is updated almost instantly, and elements build -json answers agents and humans in microseconds (build). Your terminal, your editor and every agent see the same result, and the manual is baked into the binary.
What an App Needs Past the View
React's docs are direct about what a UI library leaves out: starting without a framework means making "choices on which tools to use for routing, data fetching, and other common usage patterns. It's a lot like building your own framework" (creating a React app). A framework covers some of that list, and the database, migrations, tests, jobs, email and deploy still come from separate tools. In Elements each one is part of the same system:
- Routing. Each route is one
app.route()line inindex.ts, a web standardURLPatternmapped to a handler that runs on the server. - Sessions. Signed-cookie sessions, the same over HTTP and WebSockets.
- Database. Postgres, bundled on your machine and on each deploy target, queried with the
sql()function. It suits prototypes and single-server apps; for several servers, setDB_HOSTto a shared Postgres, self-run or managed. - Migrations. SQL files. Edit one in development and Elements synchronizes the change to the database.
- Tests. Part of the build, each in a Postgres transaction that rolls back.
- Jobs and email. Background jobs and cron with the queue in Postgres, and email with templates in Elements HTML.
- Realtime. LiveTable and channels, carried by Postgres.
- Caching. On the HTTP standard: every page has an ETag computed from its source, data and session, so an unchanged page answers 304; every asset has a hash in its file name and is cached by the browser and any CDN in between; signed-in pages stay private (server rendering).
- Deploy.
elements deployto any Ubuntu server you reach over SSH, with the load balancer and TLS set up, and incremental deploys that often finish in under a second.
Who Funds React
React is owned by the React Foundation, hosted by the Linux Foundation, whose board is made up of representatives of member companies (the React Foundation). The first framework React's docs recommend is Next.js (creating a React app), made by Vercel, a foundation member that sells hosting. Elements vs Next.js covers that framework and its business model.
React is funded by member companies with businesses of their own: Meta, where React began, builds its own apps on React (Meta engineering), and Vercel sells hosting for the framework React's docs list first. Elements is a company that sells its tooling directly, per machine, with nothing metered (pricing), so what it earns from is the tooling itself. Elements charges for the tooling, not the framework: the app framework and runtime packages such as @elements/app are MIT licensed, and everything you build is yours (licenses).
Elements Is Not React Compatible
A React component does not run in an Elements app, and that is deliberate. Elements HTML is standard HTML, the language every developer and every agent already knows, with no second dialect for attributes and no component model to learn first. Because Elements owns the language, its compiler type checks every template with the rest of the app and emits both the code that renders each page on the server and the code that runs it in the browser, and the runtime tracks which markup reads which data. That is why a change patches one binding instead of re-running a component tree, and why there is no memoization to manage. React's model of running components again and comparing the output is the model that memo, useMemo and the React Compiler exist to tune.
Moving From React
An agent can port a React app to Elements easily. Components become Elements HTML templates, state becomes plain objects, data fetching becomes route handlers and @rpc calls, and the server code goes into the same files. You keep TypeScript and your npm packages, and you gain server rendering on every page, sessions, Postgres with migrations, tests and deploy, with nothing to assemble.
Questions
Is Elements an alternative to React?
Yes. React is a UI library that its docs recommend pairing with a framework. Elements is a project server with a default framework whose pages are written in Elements HTML, and the same system includes routing, sessions, a bundled Postgres database, tests and deploy.
Does Elements ship JavaScript to the browser?
Yes. Elements HTML is a language extension to HTML plus a runtime. The Elements compiler compiles each .ehtml file to JavaScript, and the Elements runtime runs in the browser: event handlers run there, inputs bind two ways, and the runtime patches the DOM in place when data changes.
Does Elements stream UI from the server?
No. The server never renders an update and never sends HTML after a page's first response. Calls to the server, whether an @rpc function, a channel or a LiveTable, return data, and the browser renders it with the page's own templates.
Does Elements do server-side rendering?
Yes, on every page. Each page arrives as complete HTML, and the same page is fully reactive in the browser, with no compromise and no choice to make about which parts render where.
Is the Elements LiveView type the same as Phoenix LiveView?
No, the two are unrelated. In Elements, LiveView is the type of one request's rows from a LiveTable. The rows are data held in the browser and kept in sync with the server as row changes, and the browser renders them. After the page's first response, the server sends no HTML.
Does React's ecosystem matter when an AI agent writes the code?
React has more component libraries, but an AI agent builds a component from HTML and CSS faster than it can find, install and adapt one, and the result has no React version to keep in step with. The npm packages that do real work, such as Stripe and the AWS SDK, install into Elements unchanged.
Can I use React components in Elements?
No. Elements has its own HTML language, which the compiler type checks and turns into code that renders on the server and patches only what changed in the browser. An agent rebuilds a React component as an Elements HTML template easily, with no hooks or memoization left to manage.