PraxisJS

Quick Start

Create a new PraxisJS project and build your first component in minutes.

Quick Start

create-praxisjs scaffolds a project with TypeScript, Vite, JSX, and HMR already configured. It's the fastest way to get started.

npm create praxisjs@latest
pnpm create praxisjs
yarn create praxisjs
bun create praxisjs

The CLI asks for a project name and a template:

TemplateWhat's included
Minimalcore, decorators, jsx, runtime
With RouterMinimal + router
FullRouter + store, di, composables, concurrent, devtools

Start the dev server:

cd my-app
npm install
npm run dev

Manual 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-plugin
pnpm add @praxisjs/core @praxisjs/jsx @praxisjs/runtime @praxisjs/decorators
pnpm add -D @praxisjs/vite-plugin
yarn add @praxisjs/core @praxisjs/jsx @praxisjs/runtime @praxisjs/decorators
yarn add -D @praxisjs/vite-plugin

2. 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.

Storybook
Counter — live demo

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

On this page