← UI Rendering & SSR

UI RENDERING & SSR FEATURE

Reactive Report

A scheduled job changes server state, and the slot keeps reflecting that state live with no page reload and no client-side state machine.

Feature Guide

A quick orientation block that answers the essential questions: what this feature does, how it works, why it matters, and the key concepts behind it.

What this does

Reactive slots reflect changing server state as live HTML, so scheduled jobs and background work appear in the UI without page reloads.

How it works

A job updates storage, ReactiveReportSlot re-renders from DemoJobRun state on each refresh tick, and the page swaps the returned HTML into the existing shell.

Why it matters

The page feels live, but the architecture remains coherent: one SSR-first shell, one slot model, one rendering pipeline, no separate frontend state machine.

Key concepts

ReactiveReportSlot
Deferred slot resource that turns background job state into live HTML.
DemoJobRun
Stores report execution state consumed by the live slot.
SSR-first live UI
Background changes appear in the page through server-rendered slot updates, not SPA state orchestration.

Server State -> Live Slot

Scheduled work reflected as live HTML

A background report job changes server state, and the deferred slot keeps re-rendering that state as HTML. The page stays SSR-first from start to finish.

Why this matters

  • Background jobs often force teams to invent a parallel frontend state machine just to show progress.
  • Even simple status pages get split into initial SSR and later client-managed rendering logic.
  • The result feels live, but the architecture quietly drifts into a small SPA around one widget.
Pending current server-side run state
0% progress rendered from the slot
3s slot refresh interval

State Split

Job progress managed outside SSR

The page renders once, then a client-side state layer takes over to keep the progress widget alive.

The live bit stops sharing one rendering story with the rest of the page.

SSR-First Live Report

Server-rendered progress all the way through

The slot refreshes from real server state and keeps returning HTML, so the page remains conceptually server-rendered.

Waiting for next scheduled run…

refreshInterval #[AsScheduledJob] DemoJobRun SSR-first live UI

Verified against Semitexa Ultimate 2026.09.19.1020

Reactive Report

A scheduled job changes server state, and the slot keeps reflecting that state live with no page reload and no client-side state machine.

The problem

Background jobs often force teams to invent a parallel frontend state machine just to show progress. Even simple status pages get split into initial SSR and later client-managed rendering logic. The result feels live, but the architecture quietly drifts into a small SPA around one widget.

How it works

A background report job runs on a schedule and writes its progress to server storage. A deferred slot with refreshInterval set keeps its SSE connection open, and the SERVER re-renders it on that cadence and pushes the HTML down. Each render reads the latest job state and the page swaps the HTML in place. There is no request per refresh; the only requests after the first are the framework's own SSE reconnects.

The slot starts as SSR output, not as a placeholder for a client-side widget framework. Background jobs update storage, and the slot simply keeps re-rendering the current server truth.

Who may connect, and for how long

The slot rides the canonical /__semitexa_kiss stream, so the page inherits that stream's access rules rather than any of its own. By default an anonymous connection is refused with 401 Unauthorized: a status page meant for signed-out visitors will simply never start. SSE_PUBLIC_ANONYMOUS=true opens it to them.

Opening it does not remove the limits. The configured connection caps still apply, and SSE_MAX_CONNECTION_AGE_SECONDS still ends a connection that has been held too long — the client reconnects, which is an ordinary HTTP request and counts like one. A report page left open on a wallboard is a held coroutine per viewer for as long as the cap allows, so the capacity question is how many viewers, not how many refreshes.

Key mechanisms

  • refreshInterval — how often the server re-renders and pushes. Its cost is a held coroutine per connected user, which makes this a capacity decision as well as a UX one.
  • #[AsScheduledJob] — marks the background job that updates progress state.
  • DemoJobRun — stores live job state that the slot turns into HTML.
  • ReactiveReportSlot — owns the live region contract separately from the main page resource.

Why this matters

The same page can combine static SSR, deferred SSR, and live SSR without changing mental models. Keeping background job progress as server-owned state and reflecting it through slot HTML means the UI stays accurate without a client-side polling layer and without a separate data synchronization mechanism.

© Edsger W. Dijkstra: "Simplicity is prerequisite for reliability."

ReactiveReportSlot Secondary page region
<?phpdeclare(strict_types=1);use Examples\Rendering\Philosophy\Support\JobService;use Semitexa\Core\Attribute\InjectAsReadonly;use Semitexa\Ssr\Attribute\AsSlotHandler;use Semitexa\Ssr\Attribute\AsSlotResource;use Semitexa\Ssr\Application\Service\Http\Response\HtmlSlotResponse;#[AsSlotResource(    handle: 'imports_dashboard',    slot: 'job_status',    deferred: true,    refreshInterval: 3,)]final class ImportJobStatusSlot extends HtmlSlotResponse{    public function withStatus(string $status): self    {        return $this->with('status', $status);    }    public function withProgress(int $progress): self    {        return $this->with('progress', $progress);    }}#[AsSlotHandler(slot: ImportJobStatusSlot::class)]final class ImportJobStatusSlotHandler{    #[InjectAsReadonly]    protected JobService $jobs;    public function handle(ImportJobStatusSlot $slot): ImportJobStatusSlot    {        $run = $this->jobs->latestImportRun();        return $slot            ->withStatus($run->status())            ->withProgress($run->progressPercent());    }}// The slot keeps re-rendering server truth. No second client-side state machine is required.

SSR-First Live UI

Reactive slots without SPA orchestration

Reactive slots take the same deferred-slot model and add timed refresh. The live region still belongs to the server-rendered page, not to a separate frontend app.

The slot starts as SSR output, not as a placeholder for a client-side widget framework.

Background jobs update storage, and the slot simply keeps re-rendering the current server truth.

refreshInterval stays declarative on the slot resource instead of hiding in bespoke JavaScript loops.

The same page can combine static SSR, deferred SSR, and live SSR without changing mental models.

Capability What Semitexa makes explicit
refreshInterval Controls how often the slot refreshes from the server.
DemoJobRun Stores live job state that the slot turns into HTML.
ReactiveReportSlot Owns the live region contract separately from the main page resource.

How it works

A job updates storage, ReactiveReportSlot re-renders from DemoJobRun state on each refresh tick, and the page swaps the returned HTML into the existing shell.

Why it matters

The page feels live, but the architecture remains coherent: one SSR-first shell, one slot model, one rendering pipeline, no separate frontend state machine.

Key concepts

ReactiveReportSlot
Deferred slot resource that turns background job state into live HTML.
DemoJobRun
Stores report execution state consumed by the live slot.
SSR-first live UI
Background changes appear in the page through server-rendered slot updates, not SPA state orchestration.

Support Semitexa
Built for developers who prefer control over magic. Your support helps keep it fast, open, and evolving.

Donate via PayPal