Getting Started with Exodra
Exodra is a fine‑grained, no‑virtual‑DOM UI framework. You write JSX; a compiler sorts every prop into a typed bucket; the renderer wires each reactive value straight to the one DOM node it touches. No diffing, no re‑renders — the parts that change are the only parts that move.
Zero to a running app is one command:
Installation
Create a New Project
The fastest way to get started is with create-exodra:
npm create exodra@latest my-app
cd my-app
npm run dev
You'll be prompted for a few choices:
- TypeScript (or JavaScript)
- ESLint
- Tailwind CSS
src/directory- Server-side rendering (SSR)
- App Router
- Custom import alias
Each prompt has a matching flag (for example --typescript, --tailwind,
--eslint, --src-dir, --ssr, --app, --import-alias <alias>) so you can
skip the questions in CI or scripts.
Manual Setup
You can also add Exodra to an existing project:
npm install @exodra/core @exodra/dom @exodra/reactivity
npm install -D @exodra/babel-plugin-jsx
Then configure Babel to use the JSX plugin (see JSX Guide for details).
Project Structure
A typical Exodra project looks like this:
my-app/
├── src/
│ ├── components/
│ ├── pages/
│ ├── app.tsx
│ └── main.tsx
├── public/
├── package.json
├── tsconfig.json
└── vite.config.ts
Your First Component
Create a simple counter component with Exodra JSX. A bindable is a reactive
cell: read it with getValue(), write it with setValue(). There is no
.value property. To show a derived string in the DOM, build a derive() and
pass it straight into the bindable bucket.
import { bindable, derive } from '@exodra/reactivity';
export function Counter() {
const count = bindable(0);
const label = derive(count, c => String(c));
return (
<div static={{ class: 'counter' }}>
<button
static={{ class: 'btn' }}
handlers={{ onClick: () => count.setValue(count.getValue() - 1) }}
>
-
</button>
<span bindable={{ textContent: label }} />
<button
static={{ class: 'btn' }}
handlers={{ onClick: () => count.setValue(count.getValue() + 1) }}
>
+
</button>
</div>
);
}
Every prop goes in a typed bucket: static for values that never change,
bindable for reactive bindables, and handlers for events. Flat React-style
props such as <button onClick={...}> are a compile error in Exodra.
Building a Todo App
Here's a small todo app showing reactive lists. list() builds a reactive list
that you mutate with push, insert, remove, move, set, or reset — it
is not an array, so there is no .map() on the list. Build the children up front
and pass the list directly into the bindableList bucket.
import { bindable, derive, list } from '@exodra/reactivity';
import type { TExoSchema } from '@exodra/core';
export function TodoApp() {
const input = bindable('');
const todos = list<TExoSchema>([]);
const makeTodo = (text: string): TExoSchema => {
const done = bindable(false);
const style = derive(done, d =>
d ? 'text-decoration: line-through' : 'text-decoration: none'
);
return (
<li>
<input
static={{ type: 'checkbox' }}
bindable={{ checked: done }}
handlers={{ onChange: () => done.setValue(!done.getValue()) }}
/>
<span static={{ class: 'todo__text' }} bindable={{ style }}>
{text}
</span>
</li>
);
};
const addTodo = () => {
const text = input.getValue().trim();
if (!text) return;
todos.push(makeTodo(text));
input.setValue('');
};
return (
<div static={{ class: 'todo-app' }}>
<div static={{ class: 'input-group' }}>
<input
static={{ type: 'text', placeholder: 'Add todo...' }}
bindable={{ value: input }}
handlers={{
onInput: (e: Event) =>
input.setValue((e.target as HTMLInputElement).value),
onKeyDown: (e: KeyboardEvent) => {
if (e.key === 'Enter') addTodo();
},
}}
/>
<button static={{ class: 'btn' }} handlers={{ onClick: addTodo }}>
Add
</button>
</div>
<ul bindableList={{ children: todos }} />
</div>
);
}
Development Server
Start the development server:
npm run dev
Your app will be available at http://localhost:5173
Hot Module Replacement
Exodra supports HMR out of the box - changes are reflected instantly without losing application state.
Build for Production
npm run build
The optimized bundle will be in the dist/ folder.