Tina4 JavaScript - Quick Reference
🔥 Sub-3KB core, reactive framework
- Signals for state,
htmltagged templates for rendering, Web Components for reuse - Client-side routing with
{param}syntax matching tina4-php/python - Fetch API wrapper with Bearer + formToken auth compatible with tina4 backends
- PWA support with runtime manifest and service worker generation
- Tree-shakeable: import only what you need
Installation
npx tina4js create my-app
cd my-app
npm install
npm run devMore details on project setup, CLI options, and PWA scaffolding.
Signals
import { signal, computed, effect, batch } from 'tina4js';
const count = signal(0);
const doubled = computed(() => count.value * 2);
effect(() => console.log(`Count: ${count.value}`));
count.value = 5; // triggers effect → "Count: 5"
console.log(doubled.value); // 10
// Batch multiple updates into one notification
batch(() => { count.value = 10; count.value = 20; });More details on reactive state, computed values, effects, and batching.
HTML Templates
import { signal, html } from 'tina4js';
const name = signal('World');
const view = html`
<div>
<h1>Hello, ${name}!</h1>
<input @input=${(e: Event) => {
name.value = (e.target as HTMLInputElement).value;
}}>
<button @click=${() => { name.value = 'World'; }}>Reset</button>
</div>
`;
document.getElementById('root')!.appendChild(view);More details on template syntax, event handlers, boolean attributes, conditionals, and lists.
Components
import { Tina4Element, html, signal } from 'tina4js';
class MyCounter extends Tina4Element {
static props = { label: String };
static styles = `:host { display: block; }`;
count = signal(0);
render() {
return html`
<div>
<span>${this.prop('label')}: ${this.count}</span>
<button @click=${() => this.count.value++}>+</button>
</div>
`;
}
onMount() { console.log('Connected!'); }
onUnmount() { console.log('Removed!'); }
}
customElements.define('my-counter', MyCounter);<my-counter label="Clicks"></my-counter>More details on props, Shadow DOM, styles, lifecycle hooks, and events.
Routing
import { route, router, navigate, html } from 'tina4js';
route('/', () => html`<h1>Home</h1>`);
route('/user/{id}', ({ id }) => html`<h1>User ${id}</h1>`);
route('/admin', {
guard: () => isLoggedIn() || '/login',
handler: () => html`<h1>Admin</h1>`
});
route('*', () => html`<h1>404</h1>`);
router.start({ target: '#root', mode: 'history' });
// Navigate programmatically
navigate('/user/42');More details on hash vs history mode, route params, guards, and change events.
API
import { api } from 'tina4js';
api.configure({ baseUrl: '/api', auth: true });
const users = await api.get('/users');
const user = await api.get('/users/42');
await api.post('/users', { name: 'Andre' });
await api.put('/users/42', { name: 'Updated' });
await api.delete('/users/42');More details on configuration, authentication, token rotation, interceptors, and error handling.
WebSocket
import { ws } from 'tina4js';
const socket = ws.connect('/ws/chat'); // status & connected are signals - bind them in templates
const view = html`
<div>Status: ${socket.status}</div>
${() => socket.connected.value ? html`<span>● live</span>` : html`<span>○ reconnecting...</span>`}
`;
socket.on('message', (msg) => console.log(msg));
socket.send({ type: 'hello' });Auto-reconnect with exponential backoff; status/connected are signals, so the UI reacts to the connection state with no extra wiring. More details.
SSE / Streaming
import { sse } from 'tina4js';
const stream = sse.connect('/events', { json: true }); // Server-Sent Events / NDJSON
stream.on('data', (row) => append(row));Same signal-driven status + auto-reconnect shape as ws, for one-way server push and NDJSON. More details.
PWA
import { pwa } from 'tina4js';
pwa.register({
name: 'My App',
shortName: 'App',
themeColor: '#1a1a2e',
cacheStrategy: 'network-first', // or 'cache-first', 'stale-while-revalidate'
precache: ['/', '/css/styles.css'],
offlineRoute: '/offline',
});More details on manifest generation, service worker strategies, and offline support.
Storage
import { signal } from 'tina4js';
import { persist, clearPersistedKeys } from 'tina4js/storage';
const theme = persist(signal('light'), { key: 'theme' }); // survives reloads, syncs across tabs
theme.value = 'dark'; // written to localStorage automatically
clearPersistedKeys(['theme']); // remove on logoutpersist backs a signal with localStorage: versioned, migratable, and synced across tabs. Never store secrets, tokens, or personal data: localStorage is readable by any script on the page (XSS), so it is for UI preferences and non-sensitive view state only; keep auth tokens in memory or an httpOnly cookie.
Debug
// Enable the dev overlay (Ctrl+Shift+D to toggle). Dev only - tree-shaken from production.
if (import.meta.env.DEV) import('tina4js/debug');A side-effect import that mounts an overlay tracking live signals, mounted components, route changes, and API calls. Never ship it to production. More details.
Backend Integration
With tina4-php
npx tina4js build --target php
# Outputs JS to src/public/js/
# Generates src/templates/pages/index.twigThe build drops your SPA's entry point at src/templates/pages/index.twig. Tina4's auto-routing serves it at /, with no env var or route needed. If your build emits a static index.html instead, drop it at src/public/index.html and Tina4 auto-serves it at / too (since v3.11.33).
With tina4-python
npx tina4js build --target python
# Outputs JS to src/public/js/
# Generates src/templates/pages/index.twigSame auto-routing: src/templates/pages/*.twig becomes the page tree under /. Set TINA4_TEMPLATE_ROUTING=off if you want explicit routes only. More details on embedding in tina4-php/python, auth flow, and server-side state injection.
Bundle Size
What your app actually downloads. tina4-js is code-split: a bundler ships one shared reactive-core chunk plus only the modules you import. These are real deduplicated bundles (esbuild --minify, then compressed), measured on macOS, v1.2.7; brotli is what most CDNs serve:
| Your app imports | gzip | brotli |
|---|---|---|
Core only (signals + html + components) | 2.30 KB | 2.05 KB |
| Core + Router (typical SPA) | 3.14 KB | 2.78 KB |
| + API | 4.00 KB | 3.54 KB |
| + WebSocket + SSE | 5.32 KB | 4.73 KB |
| Everything (+ Storage + PWA, no Debug) | 7.52 KB | 6.68 KB |
Marginal cost per feature is small (gzip): Router +0.8 KB, API +0.9 KB, WebSocket + SSE +1.3 KB. Debug is a separate dev-only entry (import 'tina4js/debug', ~5 KB gzip): guard it behind import.meta.env.DEV so your production bundler drops it entirely.
// Import from sub-paths to help the bundler tree-shake:
import { signal, html } from 'tina4js/core';
import { route, router } from 'tina4js/router';
import { api } from 'tina4js/api';
import { ws } from 'tina4js/ws';
import { sse } from 'tina4js/sse';
import { persist } from 'tina4js/storage';
import { pwa } from 'tina4js/pwa';Don't add up the published
dist/*.es.jsfile sizes. Each looks standalone, but the reactive core lives in one shared chunk that the others import, so summing the files counts core several times over (and includes the dev-only debug overlay). The table above is what actually ships.
📕 Download the book
tina4-js: The 1.5KB Reactive Core (PDF): full reference, printable, with clickable table of contents and PDF outline. Regenerated with every release.