Quick Start
Automatic setup
The fastest way to get started is create-praxisjs, which scaffolds a project with TypeScript, Vite, JSX, and HMR already configured.
npm create praxisjs@latestpnpm create praxisjsyarn create praxisjsbun create praxisjsThe CLI will ask for a project name and template:
| Template | Packages included |
|---|---|
| Minimal | core, decorators, jsx, runtime |
| With Router | Minimal + router |
| Full | Router + store, di, composables, concurrent, devtools |
Then start the dev server:
cd my-app
npm install
npm run devManual setup
Install packages
npm install @praxisjs/core @praxisjs/jsx @praxisjs/runtime @praxisjs/decorators
npm install -D @praxisjs/vite-pluginpnpm add @praxisjs/core @praxisjs/jsx @praxisjs/runtime @praxisjs/decorators
pnpm add -D @praxisjs/vite-pluginyarn add @praxisjs/core @praxisjs/jsx @praxisjs/runtime @praxisjs/decorators
yarn add -D @praxisjs/vite-pluginConfigure Vite
// vite.config.ts
import { defineConfig } from 'vite'
import { praxisjs } from '@praxisjs/vite-plugin'
export default defineConfig({
plugins: [praxisjs({ hmr: true })],
})Configure TypeScript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@praxisjs/jsx",
"useDefineForClassFields": false,
"strict": true,
"noEmit": true
}
}useDefineForClassFields: false is required
With true (the ES2022+ default), TypeScript compiles class fields using Object.defineProperty, which runs after decorator initializers and overwrites the getter/setter they registered. Setting it to false compiles fields as constructor assignments (this.x = value), so the decorator's signal wiring stays intact.
Your first component
// src/Counter.tsx
import { StatefulComponent } from '@praxisjs/core'
import { Component, State, Prop } from '@praxisjs/decorators'
@Component()
class Counter extends StatefulComponent {
@Prop() initialCount = 0
@State() count = 0
onMount() {
this.count = this.initialCount
}
render() {
return (
<div>
<p>Count: {() => this.count}</p>
<button onClick={() => this.count++}>+</button>
<button onClick={() => this.count--}>-</button>
</div>
)
}
}Why {() => this.count} and not {this.count}?
render() runs once on mount. To keep a value reactive in the DOM, wrap it in an arrow function — the renderer tracks it as a live dependency. {this.count} captures the value at render time and never updates.
Mount the app
// src/main.ts
import { render } from '@praxisjs/runtime'
import { Counter } from './Counter'
render(() => <Counter initialCount={5} />, document.getElementById('app')!)What's next?
- Components — stateful and stateless components
- Reactivity & Signals — how signals work under the hood
- Decorators: State & Props — full decorator reference