Keep-alive and pausing

Keep a hidden tab or route mounted so its state survives, and stop the work inside it from running while nobody is looking. The Angular analog of React's Activity and Vue's keep-alive.

@mmstack/primitivesnpm

Switch away from a tab built with @switch and the old branch is destroyed: scroll position, half-filled inputs, a paused video, and loaded data all reset when you come back. Leave it mounted instead and the opposite problem appears, since its pollers, effects, and recomputes keep running in the background. These tools handle both.

#mmActivity keeps a subtree alive

*mmActivity mounts its subtree once and keeps it. While the condition is false the subtree is hidden with display: none and its change detection is paused, so state is preserved. When it is true again the view shows and change detection resumes. Nothing is destroyed until the directive itself is.

<section *mmActivity="tab() === 'editor'">
  <app-editor />  <!-- mounted once; state survives while another tab is open -->
</section>

This is the fix for a tabbed editor or a wizard where losing what the user typed on step two is unacceptable. Unlike a plain @if or @switch, the branch is never torn down.

#The paused context

*mmActivity also provides a paused context to its subtree, the negation of the visible condition. Read it anywhere below with injectPaused(), a Signal<boolean> that is true while hidden.

import { injectPaused } from '@mmstack/primitives';

// anywhere under an *mmActivity subtree
readonly paused = injectPaused(); // Signal<boolean>, true while hidden

constructor() {
  effect(() => {
    if (this.paused()) return; // skip background work while hidden
    poll(this.url());
  });
}

Pausing change detection stops pull-based work for free (templates and the computeds they read), but it does not stop effect()s or RxJS timers. A poller inside a hidden tab keeps polling unless you gate it on injectPaused(), use the pausable primitives below, or use a resource that respects the paused context. providePaused(signal) sets up your own boundary without *mmActivity. On the server nothing is ever paused, so the full tree renders.

#Pausable primitives

pausableSignal, pausableComputed, and pausableEffect are drop-in versions that suspend while paused. By default they read the ambient paused context, so dropping them inside an *mmActivity subtree just works. Pass pause for an explicit source, or pause: false to opt out, which returns the bare primitive with no wrapper overhead.

import {
  pausableSignal,
  pausableComputed,
  pausableEffect,
} from '@mmstack/primitives';

const scroll = pausableSignal(0);                        // reads hold while paused
const total = pausableComputed(() => expensive(data())); // does not recompute while paused
pausableEffect(() => poll(url()));                       // body skipped while paused

A paused computed holds its last value and does not recompute. A paused effect skips its body, and its dependencies collapse so a change cannot wake it until it resumes. Writes to a paused signal still land and surface on resume.

Both frames below read the same signal, ticking on a setInterval. The right one is a pausableComputed bound to the button. Pause it and the count freezes while the live frame keeps going, then it catches up in one step on resume.

Pause a running computation

Demo loads on scroll.

#Setting a default pause source

providePausableOptions is sets the default pause for every pausable-aware primitive within its injector scope, the pausable* family plus the opt-in integrations in stored and chunked. Like a suspense or pause boundary, it applies from where you provide it down the tree, and a nested providePausableOptions overrides it for that subtree. A call-site pause always wins. Provide it at the app root to make it the default everywhere, or on a route or component to scope it there.

import { providePausableOptions } from '@mmstack/primitives';

// within this injector scope, default pausable-aware primitives to honor
// the nearest paused boundary (an *mmActivity or providePaused above)
providers: [providePausableOptions({ pause: true })];

pause: true means honor the ambient paused context, so it needs an *mmActivity or providePaused boundary above any consumers to supply the paused state. The two compose: the boundary decides when its subtree is paused, and this decides which primitives listen.

Data fetching plugs into the same context. @mmstack/resource resources pause their polling and refetch triggers when the context is paused, and you can opt every query into it from one provider. This composes with transitions: a paused subtree does no background work, and a visible one still holds and swaps cleanly.