HypenHypen
Modules

Modules

Stateful module system for Hypen applications

Modules

Modules are the stateful building blocks of Hypen applications. They combine UI templates with TypeScript state management.

Overview

A module consists of:

  • Template (.hypen file) — The declarative UI
  • Definition (TypeScript) — State, actions, and lifecycle handlers
module Counter {
    Column {
        Text("Count: ${state.count}")
        Button { Text("+1") }
            .onClick(@actions.increment)
    }
}
import { app } from "@hypen-space/core";

export default app
  .defineState({ count: 0 })
  .onAction("increment", async ({ state }) => {
    state.count++;
  });

API Reference

defineState

Sets the initial state with TypeScript generics for type safety:

interface MyState {
  items: string[];
  loading: boolean;
}

app.defineState<MyState>({
  items: [],
  loading: false
})

onCreated

Lifecycle hook called when the module mounts. Receives (state, context?):

.onCreated(async (state, context) => {
  const data = await fetchData();
  state.items = data;

  // Access router
  if (context?.router) {
    context.router.push("/home");
  }
})

onDestroyed

Lifecycle hook called when the module unmounts. Receives (state, context?):

.onDestroyed((state, context) => {
  clearInterval(state.timerId);
})

onAction

Handler for UI actions. Receives a context object with action details, state, and global context:

.onAction<PayloadType>("actionName", async ({ action, state, context }) => {
  // action.name     - the action name
  // action.payload  - event data (typed as PayloadType)
  // action.sender   - component that dispatched the action
  // state           - current state (auto-synced via Proxy)
  // context         - GlobalContext for cross-module communication
  // context.router  - HypenRouter for navigation
})

onError

Error handler called when any action or lifecycle hook throws:

.onError(({ error, actionName, lifecycle, state }) => {
  console.error(`Error in ${actionName ?? lifecycle}:`, error.message);
  return { handled: true };  // suppress error
  // or: { rethrow: true }   // re-throw
  // or: { retry: true }     // retry (actions only)
  // or: void                 // default: log + emit
})

See Error Handling Guide for details.

onDisconnect / onReconnect / onExpire

Session lifecycle hooks for remote apps. These handle client disconnections and reconnections:

.onDisconnect(({ state, session }) => {
  // Client disconnected, but session is alive (within TTL)
})
.onReconnect(({ session, restore }) => {
  // Client reconnected with existing session
  restore(savedState);
})
.onExpire(({ session }) => {
  // Session TTL expired
})

See the TypeScript SDK for details.

build

Finalizes the module definition. Required when passing to RemoteServer.module() or HypenModuleInstance:

const myModule = app
  .defineState({ count: 0 })
  .onAction("increment", ({ state }) => { state.count++; })
  .build();

ui

For single-file components, embed the template directly using the hypen tagged template literal:

import { app, hypen, state } from "@hypen-space/core";

export default app
  .defineState({ count: 0 })
  .onAction("increment", ({ state }) => { state.count++; })
  .ui(hypen`
    Column {
      Text("Count: ${state.count}")
      Button { Text("+") }
        .onClick(@actions.increment)
    }
  `);

The .ui() method calls .build() internally and returns the finalized module definition.

State Management

State is tracked via Proxy — mutations sync automatically:

.onAction<{ item: Item }>("addItem", async ({ action, state }) => {
  state.items.push(action.payload!.item);
  // UI updates automatically — no callback needed
})

Deep Nesting

Nested object mutations are tracked automatically:

.onAction("updateAddress", ({ state }) => {
  state.user.profile.address.city = "New York";
  // Tracked at path "user.profile.address.city"
})

Batch Updates

Multiple synchronous mutations are coalesced into a single UI update. For explicit batching:

import { batchStateUpdates } from "@hypen-space/core";

.onAction("bulkUpdate", ({ state }) => {
  batchStateUpdates(state, () => {
    state.items = newItems;
    state.count = newItems.length;
  });
})

Best Practices

  1. Keep state minimal — Only store what the UI needs
  2. Use TypeScript interfaces — Define state shape for type safety
  3. Handle async properly — Show loading states during fetches
  4. Name actions descriptivelysaveDocument not handleClick
  5. Use .bind() for forms — Two-way binding avoids boilerplate action handlers

See Also