Chapter 1: Getting Started
Your First 5 Minutes
A terminal. A single command. A browser tab. In five minutes you will have a running tina4-js project with reactive state, a rendered page, and a dev server with hot reload. The counter on screen will respond to your clicks before you understand why. That is the point -- you ship first, then you learn.
1. What Is tina4-js
tina4-js is a reactive JavaScript framework under 3KB core gzipped. Seven modules, each solving one problem:
- Signals for reactive state -- no stores, no reducers, no actions
- Tagged template literals for DOM rendering -- no JSX, no virtual DOM, no compiler
- Web Components for encapsulation -- real custom elements, not a framework abstraction
- A router for SPA navigation
- An HTTP client built for tina4-php and tina4-python backends
- WebSocket with auto-reconnect and signal integration
- PWA support with one function call
- A debug overlay that shows you everything
One npm package. Zero dependencies. The entire framework weighs less than most favicons.
What It Is Not
tina4-js is not React. It has no virtual DOM. It does not diff trees. When a signal changes, the exact text node or attribute that depends on it updates. Nothing else moves.
tina4-js is not Angular. No dependency injection. No decorators. No module system. No zone.js.
tina4-js is not a meta-framework. No server-side rendering. No file-based routing. No data loading conventions. It is a client-side framework that does one thing well: build reactive UIs with the smallest possible footprint.
2. Prerequisites
You need three things. Nothing else.
- Node.js 18 or later -- check with:
node --versionYou should see v18.0.0 or higher.
- npm -- comes with Node.js. Check with:
npm --version- A text editor -- VS Code, Cursor, Zed, Vim, anything.
3. Create a Project
Two ways to scaffold. Pick whichever you have installed:
Option A: Using the Tina4 CLI (if you have tina4 installed):
tina4 init tina4js my-appOption B: Using npx (no global install needed):
npx tina4js create my-appThis scaffolds a complete project with TypeScript, Vite, routing, and a sample page.
Want the optional CSS framework included? Add the --css flag:
npx tina4js create my-app --cssThis adds tina4-css to your dependencies -- a utility CSS library with reset, grid, buttons, forms, tables, cards, and dark mode built in. More on this in Chapter 10.
Want PWA support from the start? Add --pwa:
npx tina4js create my-app --pwaYou can combine them:
npx tina4js create my-app --css --pwaNow install and run:
cd my-app
npm install
npm run devOpen http://localhost:3000. A welcome page appears with a counter. Click the minus button. Click the plus button. The number updates. No page reload. No visible delay. That is signals at work -- reactive state flowing from data to DOM without you writing a single line of update logic.
4. Project Structure
Look at what the CLI created:
my-app/
index.html # Entry point -- loads src/main.ts
package.json # Dependencies: tina4js, vite, typescript
tsconfig.json # TypeScript config
vite.config.ts # Vite dev server config
src/
main.ts # App entry -- imports routes, starts router
routes/
index.ts # Route definitions
pages/
home.ts # Home page handler
components/
app-header.ts # Example web component
public/
css/
default.css # Default stylesThe structure is intentional:
src/routes/-- Route definitions. One file per feature or group.src/pages/-- Page handler functions. Each returns a template.src/components/-- Web Components (Tina4Element subclasses).src/public/-- Static assets (CSS, images, fonts).
This is a convention, not a requirement. tina4-js does not care where your files live. But this structure scales. Every tina4-js project looks the same, which matters when you onboard new team members or let AI generate code. Consistency compounds.
5. The Entry Point
Open src/main.ts:
import { signal, computed, html, route, router, navigate, api } from 'tina4js';
import './routes/index';
// Configure API (uncomment to connect to tina4-php/python backend)
// api.configure({ baseUrl: '/api', auth: true });
// Start router
router.start({ target: '#root', mode: 'hash' });Three things happen here:
- Import everything from one package. All seven modules export from
tina4js. No sub-package installs. - Import your routes. The route file calls
route()to register paths and handlers. - Start the router. It finds
#rootin the DOM and renders matched routes into it.
The mode: 'hash' means URLs look like http://localhost:3000/#/about. For clean URLs without the hash, use mode: 'history' -- but you will need server-side URL rewriting in production.
6. Your First Signal
Open src/pages/home.ts:
import { signal, computed, html } from 'tina4js';
export function homePage() {
const count = signal(0);
const doubled = computed(() => count.value * 2);
return html`
<div class="page">
<h1>Welcome</h1>
<div class="counter">
<button @click=${() => count.value--}>-</button>
<span>${count}</span>
<button @click=${() => count.value++}>+</button>
</div>
<p class="muted">Doubled: ${doubled}</p>
</div>
`;
}Four things happen in this code. Each one is a core concept you will use in every tina4-js application.
signal(0) creates a reactive value. Read it with count.value. Write it with count.value = 5. When you write, everything that depends on it updates. Not eventually. Not on the next tick. Right now.
computed(() => count.value * 2) creates a derived signal. It reads count.value inside the function, so tina4-js knows to recompute whenever count changes. You cannot write to a computed -- it is read-only.
**html\...`** is a tagged template literal. It returns a real DocumentFragment-- actual DOM nodes, not a string, not a virtual tree. When you put${count}in the template, tina4-js creates a text node that updates in place whencount` changes. No diffing. No reconciliation. Direct DOM mutation.
@click=${() => count.value--} adds a click event listener. The @ prefix means "event handler." Since v1.0.9, event handlers are wrapped in batch(), so multiple signal writes inside one handler trigger one update.
7. Your First Route
Open src/routes/index.ts:
import { route, html } from 'tina4js';
import { homePage } from '../pages/home';
// Home
route('/', homePage);
// About
route('/about', () => html`
<div class="page">
<h1>About</h1>
<p>Built with tina4-js.</p>
<a href="/">Back home</a>
</div>
`);
// 404
route('*', () => html`
<div class="page">
<h1>404</h1>
<p>Page not found.</p>
<a href="/">Go home</a>
</div>
`);route(pattern, handler) -- pattern is always the first argument. The handler is a function that returns a template. The router calls it when the URL matches.
route('*', handler) -- the wildcard catches any URL that no other route matched. Put it last.
Links work automatically. The router intercepts clicks on <a> tags that point to same-origin paths and navigates without a page reload. You do not need a special <Link> component.
8. Adding a Component
Open src/components/app-header.ts:
import { Tina4Element, html } from 'tina4js';
class AppHeader extends Tina4Element {
static props = { title: String };
static styles = `
:host { display: block; padding: 1rem 0; border-bottom: 1px solid #e5e7eb; }
h1 { margin: 0; font-size: 1.5rem; }
nav { display: flex; gap: 1rem; margin-top: 0.5rem; }
a { color: #2563eb; text-decoration: none; }
`;
render() {
return html`
<h1>${this.prop('title')}</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
`;
}
}
customElements.define('app-header', AppHeader);Then use it in any template:
html`
<app-header title="My App"></app-header>
<div class="content">...</div>
`static props declares reactive attributes. When the title attribute changes, the component re-renders that part.
static styles scopes CSS to the component via Shadow DOM. Your styles cannot leak out. External styles cannot leak in.
this.prop('title') returns a signal for the title prop. Drop it in the template and it updates reactively.
customElements.define() registers the tag name. Use a hyphenated name -- that is a Web Components requirement, not a tina4-js thing.
9. The Build
Development is done. Time to ship:
npm run buildVite bundles everything into dist/. The tina4-js runtime adds under 6KB to your bundle. Your entire app -- framework, routes, components, everything -- will likely be smaller than React's runtime alone.
To preview the production build:
npm run preview10. What Just Happened
Five minutes. One command to scaffold. One command to install. One command to run. And you covered:
- Reactive state with
signal() - Derived state with
computed() - DOM rendering with
htmltagged templates - Event handling with
@click - A web component with
Tina4Element - Client-side routing with
route()androuter.start() - A production build with Vite
The rest of this book goes deep on each of these. But you already have a working app. You already have a production build. Everything from here is precision and power.
Summary
| What | How |
|---|---|
| Create project | npx tina4js create my-app |
| With CSS framework | npx tina4js create my-app --css |
| With PWA | npx tina4js create my-app --pwa |
| Dev server | npm run dev |
| Production build | npm run build |
| Reactive state | signal(initialValue) |
| Derived state | computed(() => expression) |
| DOM rendering | html`<p>${signal}</p>` |
| Event handling | @click=${handler} |
| Components | class X extends Tina4Element |
| Routes | route(pattern, handler) |
| Start router | router.start({ target, mode }) |