Quick Start
Create a new PraxisJS project and build your first component in minutes.
Quick Start
Automatic setup (recommended)
create-praxisjs scaffolds a project with TypeScript, Vite, JSX, and HMR already configured. It's the fastest way to get started.
npm create praxisjs@latestpnpm create praxisjsyarn create praxisjsbun create praxisjsThe CLI asks for a project name and a template:
| Template | What's included |
|---|---|
| Minimal | core, decorators, jsx, runtime |
| With Router | Minimal + router |
| Full | Router + store, di, composables, concurrent, devtools |
Start the dev server:
cd my-app
npm install
npm run devManual setup
Prefer to wire things up yourself? Here's what you need.
1. 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-plugin2. Configure Vite
// vite.config.ts
import { defineConfig } from 'vite'
import { praxisjs } from '@praxisjs/vite-plugin'
export default defineConfig({
plugins: [praxisjs({ hmr: true })],
})3. 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
This is the most common setup mistake. With true (the TypeScript ES2022+ default), class fields compile as Object.defineProperty, which runs after decorator initializers and overwrites the signal getter/setter they registered. Setting it to false compiles fields as constructor assignments (this.x = value), keeping decorator wiring 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} instead of {this.count}?
render() runs once on mount. Without the arrow function, this.count is read once at that moment and never updates. Wrapping it in {() => this.count} creates a live subscription: when count changes, only that specific <p> node 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 — understand StatefulComponent vs StatelessComponent
- Reactivity & Signals — how signals and arrow functions work under the hood
- JSX Syntax — every JSX pattern: conditionals, lists, events, refs