Ragot

Ragot Overview

The browser runtime that SPRAG's codegen targets — what it is and when to reach for it directly.

Ragot Overview

Ragot is the browser-side JavaScript runtime that SPRAG compiles to. You never import Ragot directly — you write Python Component and Module subclasses, and sprag build compiles them to Ragot ESM JavaScript.

What Ragot provides

Under the hood, your compiled code uses:

  • morphDOM diffing for efficient DOM updates when state changes
  • Keyed list reconciliation (renderList) for ui.For() loops
  • Grid layout engine (renderGrid) for ui.Grid() layouts
  • Lazy loading (createLazyLoader) for ui.LazyImage() images
  • Lifecycle-safe socket binding for real-time clients supplied by SPRAG or your own browser code
  • Animation helpers (animateIn/animateOut) for CSS transitions
  • Virtual scrolling for large lists

You don't interact with any of these directly. They're the compilation targets for SPRAG's Python surface.

When to reach for raw Ragot

Occasionally you need a browser-only escape hatch. SPRAG exposes those through Python authoring stubs:

Socket clients

Ragot does not bundle Socket.IO or open a socket connection on its own. Its socket helpers bind handlers to a client object and clean those handlers up with the Module lifecycle.

In normal SPRAG apps, Module.on_socket(...), Module.off_socket(...), Module.emit_socket(...), and topics use SPRAG's shared realtime bridge. That bridge is documented in Realtime and uses the browser's native WebSocket API.

If you are writing browser-only Ragot code outside SPRAG's bridge, pass a client object with on, off, and emit methods to Ragot's socket helpers. A Socket.IO browser client can fit that shape, but it is an app-level dependency, not a Ragot or SPRAG runtime dependency.

Module imports

Declare JS dependencies on the page manifest and use them in your Module:

# page.py
analytics_page = page(
    path="/analytics",
    controller=AnalyticsController,
    screen=AnalyticsScreen,
    mode="hybrid",
    modules={"chart": "/vendor/chart.esm.js"},
)
# modules.py — the import is available via imports.*
class ChartModule(Module):
    def on_start(self):
        chart_lib = imports.chart
        self.chart = chart_lib.create(self.element.querySelector(".chart"))

Browser globals

Access globalThis.* via browser.*:

class TimerModule(Module):
    def on_start(self):
        self.width = browser.innerWidth
        browser.console.log("Module started")

Inspecting compiled output

To see what your Python compiles to:

sprag inspect /counter

This shows the generated JavaScript with source location comments mapping back to your Python.

Decision Matrix

Use this to decide which primitive is right for the job.

SituationUseDo Not Default To
Parent owns child component lifecycleadoptComponentregistry lookup
Parent owns child module lifecycleadoptbus events
Parent pushes module state into child UIadoptComponent(..., { sync })registry indirection
Shared writable state across independent modulescreateStateStorebus-only state transfer
App startup service registrationragotRegistry.providemanual globals
Service may appear laterwaitForCancellableplain waitFor
One event, many independent listenersbusregistry call chains

Common Pitfalls

1. watchState signature: The first argument must be a function. 2. subscribe signature: The first argument must be a function. 3. adopt() defaults: adopt() defaults to the stop method; components usually need unmount. 4. Detached mounting: Mounting into detached containers breaks measurements and observers. 5. Keyed siblings: Mixing keyed and unkeyed siblings in morphDOM causes ordering issues. Use data-ragot-key consistently. 6. Socket validation: Calling onSocket with a non-socket first argument logs a warning and skips binding. 7. Pending waitFor: Awaiting waitFor(...) without cancellation in lifecycle owners can leak pending handles. 8. Registry for ownership: Using the registry for parent-owned child wiring makes ownership and teardown ambiguous. 9. Logic split: Implementing the same behavior through both direct calls and bus events causes split logic paths.

Add --rebuild to force a fresh compile:

sprag inspect /counter --rebuild