# Elements vs React
React is an open-source JavaScript library that describes itself as ["the library for web and native user interfaces"](https://react.dev). 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](https://react.dev/learn/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](https://react.dev/reference/rsc/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](https://react.dev/blog/2026/02/24/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](https://react.dev/learn/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](/learn/man/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](/learn/man/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](/learn/man/html/reactivity) | 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](/learn/man/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`](/learn/man/rpc) calls and [LiveTable](/learn/man/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](/learn/man/livetable) and [channels](/learn/man/channel) | Not included |
| Build errors | [One build](/learn/man/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](/learn/man/html/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](/learn/man/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](/learn/man/html/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](/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](https://react.dev/learn/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](https://react.dev/learn/react-compiler/introduction)).
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](https://react.dev/learn/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:
```tsx
// app/todos.tsx (React)
function toggle(id: string) {
setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));
}
```
In Elements, it changes the field:
```ehtml
// app/pages/todos/template.ehtml (Elements)
export interface Todo {
id: string;
title: string;
done: boolean;
}
function toggle(todo: Todo) {
todo.done = !todo.done;
}
{todos.filter(todo => !todo.done).length} left
```
The class on that one `
` 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](/learn/man/html/reactivity)).
Hooks bring rules of their own. React's [Rules of Hooks](https://react.dev/reference/rules/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](https://react.dev/reference/eslint-plugin-react-hooks) 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?](https://github.com/react/react/issues/16956)", 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](https://react.dev/learn/scaling-up-with-reducer-and-context)). Past that, apps add a state library such as [Redux](https://redux.js.org/) or [Zustand](https://github.com/pmndrs/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](/learn/man/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](/learn/man/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](https://react.dev/learn/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](https://nextjs.org/docs/app/getting-started/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:
```ehtml
// app/pages/search/template.ehtml (Elements)
import { sql } from "@elements/app";
export interface Product {
id: string;
name: string;
price: number;
}
/** @rpc */
export function search(text: string): Product[] {
return sql(`
select
id,
name,
price
from products
where name ilike ${"%" + text + "%"}
order by name
limit 20
`).all();
}
{product.name}: {product.price}
```
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](/learn/man/async)), and no client cache to invalidate. For rows that change while the page is open, a [LiveTable](/learn/man/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](https://nextjs.org/docs/app/getting-started/server-and-client-components)). Server Functions are marked with `"use server"` ([Server Functions](https://react.dev/reference/rsc/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](/learn/man/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 ``, a modal is a `