import { Store } from '@geajs/core'Extend Store, declare reactive properties as class fields, add methods that mutate them, and export a singleton instance.
import { Store } from '@geajs/core'
class TodoStore extends Store {
todos: Todo[] = []
filter: 'all' | 'active' | 'completed' = 'all'
draft = ''
add(text?: string): void {
const t = (text ?? this.draft).trim()
if (!t) return
this.draft = ''
this.todos.push({ id: uid(), text: t, done: false })
}
toggle(id: string): void {
const todo = this.todos.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
remove(id: string): void {
this.todos = this.todos.filter(t => t.id !== id)
}
setFilter(filter: 'all' | 'active' | 'completed'): void {
this.filter = filter
}
get filteredTodos(): Todo[] {
const { todos, filter } = this
if (filter === 'active') return todos.filter(t => !t.done)
if (filter === 'completed') return todos.filter(t => t.done)
return todos
}
get activeCount(): number {
return this.todos.filter(t => !t.done).length
}
}
export default new TodoStore()The store instance is wrapped in a deep Proxy. Any mutation — direct assignment, nested property change, or array method call — is automatically tracked and batched.
this.count++
this.user.name = 'Alice'
this.todos.push({ id: '1', text: 'New', done: false })
this.items.splice(2, 1)
this.items.sort((a, b) => a.order - b.order)
this.todos = this.todos.filter(t => !t.done)Changes are batched via queueMicrotask.
const unsubscribe = store.observe([], (value, changes) => {
console.log('State changed:', changes)
})
store.observe('todos', (value, changes) => {
console.log('Todos changed:', value)
})
store.observe('user.profile.name', (value, changes) => {
console.log('Name changed to:', value)
})
unsubscribe()| Param | Type | Description |
|---|---|---|
path |
string | string[] |
Dot-separated path or array of path parts. Empty string/array observes all changes. |
handler |
(value: any, changes: StoreChange[]) => void |
Called with the current value and the batch of changes. |
Returns: () => void — call to unsubscribe.
interface StoreChange {
type: 'add' | 'update' | 'delete' | 'append' | 'reorder' | 'swap'
property: string
target: any
pathParts: string[]
newValue?: any
previousValue?: any
start?: number
count?: number
permutation?: number[]
arrayIndex?: number
leafPathParts?: string[]
isArrayItemPropUpdate?: boolean
otherIndex?: number
}Executes a function that may mutate the store without triggering observers. Pending changes are discarded after the function returns.
store.silent(() => {
store.items.splice(fromIndex, 1)
store.items.splice(toIndex, 0, draggedItem)
})Useful for drag-and-drop, bulk imports, or any case where you manage DOM updates yourself.
| Method | Change type |
|---|---|
push(...items) |
append |
pop() |
delete |
shift() |
delete |
unshift(...items) |
add (per item) |
splice(start, deleteCount, ...items) |
delete + add (or append) |
sort(compareFn?) |
reorder with permutation |
reverse() |
reorder with permutation |
Iterator methods (map, filter, find, findIndex, forEach, some, every, reduce, indexOf, includes) are intercepted to provide proxied items with correct paths.
import { Component } from '@geajs/core'import { Component } from '@geajs/core'
import counterStore from './counter-store'
export default class Counter extends Component {
template() {
return (
<div class="counter">
<span>{counterStore.count}</span>
<button click={counterStore.increment}>+</button>
<button click={counterStore.decrement}>-</button>
</div>
)
}
}| Method | When called |
|---|---|
created(props) |
After constructor, before render |
onAfterRender() |
After DOM insertion and child mounting |
onAfterRenderAsync() |
Next requestAnimationFrame after render |
dispose() |
Removes from DOM, cleans up observers and children |
Use declare props for TypeScript type-checking and prop autocompletion:
export default class TodoItem extends Component {
declare props: {
todo: { id: string; text: string; done: boolean }
onToggle: () => void
onRemove: () => void
}
template({ todo, onToggle, onRemove }: this['props']) {
return (
<li>
<input type="checkbox" checked={todo.done} change={onToggle} />
<span>{todo.text}</span>
<button click={onRemove}>x</button>
</li>
)
}
}declare props defines the component's accepted attributes (no JavaScript emitted). Adding : this['props'] to the template() parameter is optional but recommended — it types the destructured variables inside the method body for full end-to-end type safety.
| Property | Type | Description |
|---|---|---|
id |
string |
Unique component identifier (auto-generated) |
el |
HTMLElement |
Root DOM element (created lazily from template()) |
props |
(typed via declare props) |
Properties passed to the component |
rendered |
boolean |
Whether the component has been rendered |
| Method | Description |
|---|---|
$(selector) |
First matching descendant (scoped querySelector) |
$$(selector) |
All matching descendants (scoped querySelectorAll) |
const app = new MyApp()
app.render(document.getElementById('app'))render(rootEl, index?) inserts the component's DOM element into the given parent. Components render once; subsequent state changes trigger surgical DOM patches.
Generated by the Vite plugin for event delegation. Maps event types to selector-handler pairs:
get events() {
return {
click: {
'.btn-add': this.addItem,
'.btn-remove': this.removeItem
},
change: {
'.checkbox': this.toggle
}
}
}Props follow JavaScript's native value semantics:
- Primitives (numbers, strings, booleans) are passed by value. The child receives a copy. Reassigning the prop in the child does not affect the parent.
- Objects and arrays are passed by reference. The child receives the parent's reactive proxy. Mutating properties on the object or calling array methods updates the parent's state and DOM automatically.
// parent passes object and primitive
<Child user={this.user} count={this.count} />
// In the child:
this.props.user.name = 'Bob' // two-way — updates parent's DOM
this.props.count = 99 // one-way — only child's DOM updatesWhen the parent updates a prop, the new value flows down to the child, overwriting any local reassignment the child may have made to a primitive.
For objects and arrays, reactivity propagates at any depth. A grandchild receiving the same object proxy can mutate it, and every ancestor observing that data updates.
export default function TodoInput({ draft, onDraftChange, onAdd }) {
const handleKeyDown = e => {
if (e.key === 'Enter') onAdd()
}
return (
<div class="todo-input-wrap">
<input
type="text"
placeholder="What needs to be done?"
value={draft}
input={onDraftChange}
keydown={handleKeyDown}
/>
<button click={onAdd}>Add</button>
</div>
)
}Function components are converted to class components at build time by the Vite plugin.
| Gea | HTML equivalent | Notes |
|---|---|---|
class="foo" |
class="foo" |
Use class, not className |
class={`btn ${active ? 'on' : ''}`} |
Dynamic class | Template literal |
value={text} |
value="..." |
For inputs |
checked={bool} |
checked |
For checkboxes |
disabled={bool} |
disabled |
For buttons/inputs |
aria-label="Close" |
aria-label="Close" |
ARIA pass-through |
<button click={handleClick}>Click</button>
<input input={handleInput} />
<input change={handleChange} />
<input keydown={handleKeyDown} />
<input blur={handleBlur} />
<input focus={handleFocus} />
<span dblclick={handleDoubleClick}>Text</span>
<form submit={handleSubmit}>...</form>Both native-style (click, change) and React-style (onClick, onChange) names are supported.
Supported: click, dblclick, input, change, keydown, keyup, blur, focus, mousedown, mouseup, submit, dragstart, dragend, dragover, dragleave, drop.
With @geajs/mobile: tap, longTap, swipeRight, swipeUp, swipeLeft, swipeDown.
<span>{count}</span>
<span>{user.name}</span>
<span>{activeCount} {activeCount === 1 ? 'item' : 'items'} left</span>Inline style objects with camelCase property names are supported:
<div style={{ backgroundColor: 'red', fontSize: '14px' }}>Styled</div>
<div style={{ color: this.textColor }}>Dynamic</div>
<div style="color:red">String style</div>Static objects are compiled to CSS strings at build time. Dynamic values are converted to cssText at runtime.
export default class Canvas extends Component {
canvasEl = null
template() {
return (
<div>
<canvas ref={this.canvasEl} width="800" height="600"></canvas>
</div>
)
}
}Assigns the DOM element to the component property after render. Available in onAfterRender() and event handlers.
{step === 1 && <StepOne onContinue={() => store.setStep(2)} />}
{!paymentComplete ? <PaymentForm /> : <div class="success">Done</div>}Compiled into <template> markers with swap logic.
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={() => store.toggle(todo.id)} />
))}
</ul>The key prop is required. Gea uses applyListChanges for efficient add, delete, append, reorder, and swap operations. By default the runtime uses item.id when available. You can use any property as the key (key={option.value}), or the item itself for primitives (key={tag}).
import { router, Router, RouterView, Link, matchRoute } from '@geajs/core'
import type { RouteConfig, RouteMatch, RouteParams, RouteComponent } from '@geajs/core'The router singleton is a Store that tracks the current URL state. Its properties are reactive.
| Property | Type | Description |
|---|---|---|
path |
string |
Current pathname (e.g. '/users/42') |
hash |
string |
Current hash (e.g. '#section') |
search |
string |
Current search string (e.g. '?q=hello&page=2') |
query |
Record<string, string> |
Parsed key-value pairs from search (getter) |
| Method | Description |
|---|---|
navigate(path) |
Push a new history entry and update path, hash, search |
replace(path) |
Replace the current history entry (no new back-button entry) |
back() |
Go back one entry (history.back()) |
forward() |
Go forward one entry (history.forward()) |
router.navigate('/users/42?tab=posts#bio')
console.log(router.path) // '/users/42'
console.log(router.search) // '?tab=posts'
console.log(router.hash) // '#bio'
console.log(router.query) // { tab: 'posts' }Pure function that tests a URL path against a route pattern and extracts named parameters.
| Param | Type | Description |
|---|---|---|
pattern |
string |
Route pattern with optional :param and * segments |
path |
string |
Actual URL pathname to match against |
Returns: RouteMatch | null
interface RouteMatch {
path: string
pattern: string
params: Record<string, string>
}| Pattern | Matches | Params |
|---|---|---|
/about |
/about |
{} |
/users/:id |
/users/42 |
{ id: '42' } |
/files/* |
/files/docs/readme.md |
{ '*': 'docs/readme.md' } |
/repo/:owner/* |
/repo/dashersw/src/index.ts |
{ owner: 'dashersw', '*': 'src/index.ts' } |
Renders the first matching route from a routes array. Observes router.path and swaps the rendered component when the URL changes.
| Prop | Type | Description |
|---|---|---|
routes |
RouteConfig[] |
Array of { path, component } objects. First match wins. |
<RouterView routes={[
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/users/:id', component: UserProfile },
]} />Both class components and function components are supported. Matched params are passed as props. Navigating between URLs that match the same pattern updates props instead of re-creating the component.
Renders an <a> tag for SPA navigation. Left-clicks call router.push() (or router.replace() with the replace prop) instead of triggering a full page reload. Modifier-key clicks (Cmd, Ctrl, Shift, Alt), non-left-button clicks, and external URLs pass through to the browser.
| Prop | Type | Required | Description |
|---|---|---|---|
to |
string |
Yes | Target path |
label |
string |
No | Text content (alternative to children) |
children |
string |
No | Inner HTML content: <Link to="/about">About</Link> |
class |
string |
No | CSS class(es) for the <a> tag |
replace |
boolean |
No | Use router.replace() instead of router.push() |
target |
string |
No | Link target (e.g. _blank) |
rel |
string |
No | Link relationship (e.g. noopener) |
onNavigate |
(e: MouseEvent) => void |
No | Callback fired before SPA navigation |
<Link to="/about" label="About" />
<Link to="/about">About</Link>
<Link to="/users/1" class="nav-link">Alice</Link>
<Link to="/external" target="_blank" rel="noopener">Docs</Link>import { View, ViewManager, GestureHandler, Sidebar, TabView, NavBar, PullToRefresh, InfiniteScroll } from '@geajs/mobile'Full-screen Component rendering to document.body by default.
| Property | Type | Default | Description |
|---|---|---|---|
index |
number |
0 |
Z-axis position |
supportsBackGesture |
boolean |
false |
Enable swipe-back |
backGestureTouchTargetWidth |
number |
50 |
Touch area width (px) |
hasSidebar |
boolean |
false |
Allow sidebar reveal |
| Method | Description |
|---|---|
onActivation() |
Called when the view becomes active in a ViewManager |
panIn(isBeingPulled) |
Animate into viewport |
panOut(isBeingPulled) |
Animate out of viewport |
| Method | Description |
|---|---|
pull(view, canGoBack?) |
Navigate forward. canGoBack saves history. |
push() |
Go back to previous view. |
setCurrentView(view, noDispose?) |
Set active view without animation. |
canGoBack() |
true if history exists. |
toggleSidebar() |
Toggle sidebar. |
getLastViewInHistory() |
Last view in the stack. |
Supported gestures: tap, longTap, swipeRight, swipeLeft, swipeUp, swipeDown.
| Component | Description |
|---|---|
Sidebar |
Slide-out navigation panel |
TabView |
Tab-based view switching |
NavBar |
Top navigation bar with back/menu buttons |
PullToRefresh |
Pull-down-to-refresh (emits SHOULD_REFRESH) |
InfiniteScroll |
Load-more-on-scroll (emits SHOULD_LOAD) |
import { defineConfig } from 'vite'
import { geaPlugin } from '@geajs/vite-plugin'
export default defineConfig({
plugins: [geaPlugin()]
})import App from './app'
import './styles.css'
const app = new App()
app.render(document.getElementById('app'))npm create gea@latest my-app
cd my-app
npm install
npm run dev