A reactive framework for user interfaces.

Retend gives you JSX, reactive state, and the tools to build complete applications for the web, desktop and everywhere else.

Tickets.tsx
function Tickets() {
  const quantity = Cell.source(2);
  const seatsLeft = Cell.derived(() => 12 - quantity.get());
  const total = Cell.derived(() => quantity.get() * 48);

  const add = () => quantity.set(quantity.get() + 1);

  return (
    <>
      <button onClick={add}>Add a ticket</button>
      <p>{seatsLeft} of 12 seats left</p>
      <button type="submit">Pay ${total}</button>
    </>
  );
}
quantity 2total 96
TripNight train to Lisbon
DepartsFriday 14 November, 22:40
Tickets2at $48 each
Seats left10 of 12

State changes and your interface follows.

In Retend, state is something you can pass around and render directly. Change it anywhere, and everything that depends on it stays up to date.

12 of 12 cities

  • AccraGhana
  • Buenos AiresArgentina
  • Cape TownSouth Africa
  • IstanbulTürkiye
  • KyotoJapan
  • LagosNigeria
  • LisbonPortugal
  • MarrakeshMorocco
  • Mexico CityMexico
  • NairobiKenya
  • OsloNorway
  • VancouverCanada
Sourcequery0×
Sourceorder0×
Derivedmatches0×
Derivedsorted0×
Renderscount0×
Renderslist0×

Change state anywhere

query and order are Cells. The search field and the sort button set them, and everything that reads them updates.

How Cells work

Plain functions and plain JavaScript, top to bottom.

Just write the interface.

Components are functions. State is ordinary JavaScript you can read and change. Events are events. JSX describes what belongs on screen.

TaskList.tsx
import { Cell, For, If } from 'retend';
import { Input } from 'retend-utils/components';

export function TaskList() {
  const draft = Cell.source('');
  const tasks = Cell.source([{ id: 1, title: 'Ship the docs', done: false }]);
  const remaining = Cell.derived(
    () => tasks.get().filter((task) => !task.done).length
  );

  const addTask = () => {
    const title = draft.get().trim();
    if (!title) return;
    tasks.set([...tasks.get(), { id: Date.now(), title, done: false }]);
    draft.set('');
  };

  return (
    <form onSubmit--prevent={addTask}>
      <Input type="text" model={draft} />
      <ul>
        {For(tasks, (task) => <li>{task.title}</li>, { key: 'id' })}
      </ul>
      {If(remaining, {
        true: () => <p>{remaining} left to do</p>,
        false: () => <p>All done.</p>,
      })}
    </form>
  );
}

No dependency arrays

A derived value finds the state it reads by itself. Read tasks inside it, and it recomputes when tasks changes.

JSX and components

Not just for the web.

One app, many renderers

Retend was built without tying its programming model to the browser. Build for the DOM today, a native GPUI window tomorrow, or create a renderer for something entirely different.

Read about the rendering architecture
yoursdesktopserverweb</></></></>Your appComponents, Cells, routesretendTracks state, runs componentsRenderersWeb, desktop, server, yours

Enough to build the whole app.

router.ts
import { Router, defineRoutes, lazy } from 'retend/router';

const routes = defineRoutes([
  { path: '/', component: Home },
  {
    path: '/dashboard',
    component: DashboardLayout,
    children: [
      { path: 'settings', component: lazy(() => import('./Settings')) },
    ],
  },
]);

export const router = new Router({ routes, middlewares: [auth] });
  • Nested routes
  • Lazy loading
  • Middleware
  • View transitions

Routing

Describe your pages as a tree of routes. Layouts wrap the pages inside them, a page can load the first time someone visits it, and middleware can redirect before anything renders.

Read about routing

Pick a surface. Start building.

Somewhere else entirely? Build a renderer and take Retend with you.

Make something cool.

pnpm dlx retend-start@latest my-app

Read the docs