Skip to content

airxjs/airx

Repository files navigation

Airx ☁️

npm build status TypeScript MIT License

A lightweight, Signal-driven JSX web application framework

中文文档English Documentation

Airx is a modern frontend framework built on JSX and Signal primitives, designed to provide a simple, performant, and intuitive solution for building reactive web applications.

✨ Features

  • 🔄 Signal-driven reactivity: Seamlessly integrates with TC39 Signal proposal
  • 📝 TypeScript-first: Developed entirely in TypeScript with excellent type safety
  • Functional components: Define components using clean JSX functional syntax
  • 🚫 No hooks complexity: Simple and straightforward API without React-style hooks
  • 🪶 Lightweight: Minimal bundle size with zero dependencies
  • 🔌 Extensible: Plugin system for advanced functionality
  • 🌐 Universal: Works in both browser and server environments

🚀 Quick Start

Installation

npm install airx
# or
yarn add airx
# or
pnpm add airx

Basic Usage

import * as airx from 'airx'
import { Signal } from 'signal-polyfill'

// Create reactive state using Signal
const count = new Signal.State(0)
const doubleCount = new Signal.Computed(() => count.get() * 2)

function Counter() {
  const localState = new Signal.State(0)

  const increment = () => {
    count.set(count.get() + 1)
    localState.set(localState.get() + 1)
  }

  // Return a render function
  return () => (
    <div>
      <h1>Counter App</h1>
      <p>Global count: {count.get()}</p>
      <p>Double count: {doubleCount.get()}</p>
      <p>Local count: {localState.get()}</p>
      <button onClick={increment}>
        Click me!
      </button>
    </div>
  )
}

// Create and mount the app
const app = airx.createApp(<Counter />)
app.mount(document.getElementById('app'))

🌐 Server-Side Rendering (SSR)

Airx supports server-side rendering (SSR) out of the box. SSR allows you to render your components to HTML strings on the server, which can improve initial page load performance and SEO.

📦 SSR APIs (createSSRApp, hydrate, renderToString) are available via the airx/server subpath export:

import { createSSRApp, hydrate, renderToString } from 'airx/server'

Quick Start

import * as airx from 'airx'

// Create an SSR app
const app = airx.createSSRApp(<MyComponent />)

// Render to HTML string
const html = await app.renderToString()
// html === '<div><h1>Hello World</h1></div>'

Full SSR Example

import { createSSRApp } from 'airx/server'

// Define a component
function UserCard({ name, email }: { name: string; email: string }) {
  return () => (
    <div className="user-card">
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  )
}

// Server-side rendering
async function renderPage() {
  const app = createSSRApp(
    <UserCard name="Alice" email="alice@example.com" />
  )
  
  const html = await app.renderToString()
  console.log(html)
  // <div class="user-card"><h2>Alice</h2><p>alice@example.com</p></div>
  
  return html
}

Hydration (Client-Side Activation)

✅ Hydration is stable in 0.8.0+ for activating server-rendered HTML on the client.

import { createSSRApp, hydrate } from 'airx/server'

// Server: render HTML
const app = createSSRApp(<App />)
const ssrHtml = await app.renderToString()
// Send ssrHtml to client...

// Client: activate SSR HTML
const container = document.getElementById('app')
if (container) {
  container.innerHTML = ssrHtml
  hydrate(ssrHtml, container, app)
  // App is now interactive!
}

API Reference

createSSRApp(element)

Creates an SSR application instance for server-side rendering.

const app = airx.createSSRApp(<MyComponent />)

app.renderToString()

Renders an SSR app to an HTML string (returns a Promise).

const html = await app.renderToString()

hydrate(element, container, options?)

Activates server-rendered HTML on the client for interactive updates.

const hydrated = hydrate(<App />, container, {
  stateSnapshot,  // optional: Signal state from SSR
  forceReset: false  // optional: skip SSR state, recalculate from scratch
})

// hydrated.unmount() - cleanup function

📖 Core Concepts

Components

Components in Airx are simple functions that return a render function:

function MyComponent() {
  const state = new Signal.State('Hello')
  
  return () => (
    <div>{state.get()} World!</div>
  )
}

State Management

Airx leverages the Signal primitive for reactive state management:

// State
const count = new Signal.State(0)

// Computed values
const isEven = new Signal.Computed(() => count.get() % 2 === 0)

// Effects
const effect = new Signal.Effect(() => {
  console.log('Count changed:', count.get())
})

Context & Dependency Injection

const ThemeContext = Symbol('theme')

function App() {
  // Provide values down the component tree
  airx.provide(ThemeContext, 'dark')
  
  return () => <Child />
}

function Child() {
  // Inject values from parent components
  const theme = airx.inject(ThemeContext)
  
  return () => (
    <div className={`theme-${theme}`}>
      Current theme: {theme}
    </div>
  )
}

Lifecycle Hooks

function Component() {
  airx.onMounted(() => {
    console.log('Component mounted')
    
    // Return cleanup function
    return () => {
      console.log('Component unmounted')
    }
  })

  airx.onUnmounted(() => {
    console.log('Component will unmount')
  })
  
  return () => <div>My Component</div>
}

## 📚 API Reference

Airx follows a minimal API design philosophy. Here are the core APIs:

### `createApp(element)`

Creates an application instance.

```tsx
const app = airx.createApp(<App />)
app.mount(document.getElementById('root'))

provide<T>(key, value): ProvideUpdater<T>

Provides a value down the component tree through context. Must be called synchronously within a component.

function Parent() {
  airx.provide('theme', 'dark')
  return () => <Child />
}

inject<T>(key): T | undefined

Retrieves a provided value from the component tree. Must be called synchronously within a component.

function Child() {
  const theme = airx.inject('theme')
  return () => <div>Theme: {theme}</div>
}

onMounted(listener): void

Registers a callback for when the component is mounted to the DOM.

type MountedListener = () => (() => void) | void

airx.onMounted(() => {
  console.log('Mounted!')
  return () => console.log('Cleanup')
})

onUnmounted(listener): void

Registers a callback for when the component is unmounted from the DOM.

type UnmountedListener = () => void

airx.onUnmounted(() => {
  console.log('Unmounted!')
})

createElement(type, props, ...children)

Creates virtual DOM elements (usually handled by JSX transpiler).

Fragment

A component for grouping multiple elements without adding extra DOM nodes.

function App() {
  return () => (
    <airx.Fragment>
      <div>First</div>
      <div>Second</div>
    </airx.Fragment>
  )
}

ErrorBoundary

A component that catches JavaScript errors in child components and displays a fallback UI.

import { ErrorBoundary } from 'airx'

function App() {
  return (
    <ErrorBoundary fallback={(error) => <div>Error: {error.message}</div>}>
      <Child />
    </ErrorBoundary>
  )
}

🔧 Development

Building from Source

# Clone the repository
git clone https://github.com/airxjs/airx.git
cd airx

# Install dependencies
npm install

# Build the project
npm run build

# Run tests
npm test

# Run tests with UI
npm run test:ui

# Run tests with coverage
npm run test:coverage

Project Structure

source/
├── app/           # Application creation and management
├── element/       # Virtual DOM and JSX handling
├── logger/        # Internal logging utilities
├── render/        # Rendering engine
│   ├── basic/     # Core rendering logic
│   ├── browser/   # Browser-specific rendering
│   └── server/    # Server-side rendering
├── signal/        # Signal integration
├── symbol/        # Internal symbols
└── types/         # TypeScript type definitions

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Workflow

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Add tests for your changes
  5. Ensure all tests pass (npm test)
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

🌐 Ecosystem & Version Compatibility

Airx 0.9.0 is compatible with the following ecosystem packages:

Package Version Peer Dependency
airx-router 0.9.0 airx: ^0.9.0
vite-plugin-airx 0.9.0 airx: ^0.9.0

Note: Previous versions of ecosystem packages (≤0.7.x) specified airx: ^0.7.1 as peer dependency. When upgrading to airx 0.9.0, please also upgrade ecosystem packages to their 0.9.0 versions.

# Install airx 0.9.0 with compatible ecosystem packages
npm install airx airx-router vite-plugin-airx

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Thanks to all contributors and supporters of the Airx project
  • Inspired by the TC39 Signal proposal
  • Built with ❤️ by the Airx community

📞 Support


Made with ☁️ by the Airx team

About

☁️ Airx is a simple enough JSX web application framework.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors