Morphos & Kosmesis are here.Explore
PraxisJS

@praxisjs/core

Changelog for @praxisjs/core — signals, computed values, effects, peek, untrack, and batch.

@praxisjs/core

2.3.1

Depends on the other @praxisjs/* packages through caret ranges (^2.3.0) instead of exact versions, so npm no longer installs a second copy of a package when two releases disagree, and upgrades no longer end in peer conflicts.

2.3.0

The published package no longer contains the package's tests (compiled tests in dist/__tests__, test sources in src/__tests__).

Smaller production bundles. The package is marked "sideEffects": false, so bundlers leave out the modules an app does not use (an app without selector() no longer carries its bucket class). The graph checks a boolean, not the observer itself, before reporting to a reactive observer: a minifier removes that instrumentation entirely from an app that never installs one, which is every app without devtools.

Added selector(source, equals?) to @praxisjs/core/internal. It returns a (key) => boolean check whose subscribers are grouped by key, so a change to the source re-runs only the effects that read the previous or the next key instead of every effect that compared against it. With the default comparison (SameValueZero) the two affected keys are looked up directly; a custom equals checks every key in use and notifies only those whose result changed. A key's subscriber list is dropped when its last effect stops, and the source subscription is released a microtask after the last key is gone. Read outside an effect, the check always compares against the current source value, also right after a write inside a batch or an event handler, before the subscribers are notified.

New reactive graph. Signals, computeds and effects were rewritten as a push-pull graph:

  • Dependencies are linked nodes that a re-run reuses in place. A binding that reads the same sources every time allocates nothing and no longer unsubscribes and resubscribes on each run, and a notification no longer copies the subscriber list.
  • A computed recomputes only when one of its sources actually changed (tracked with versions), and a computed that recomputes to the same value (Object.is) does not wake its dependents.
  • Effects that read a changed signal directly re-run synchronously, as before. Effects that depend on a change only through a computed still re-run once per tick in a microtask, but they now pull fresh values when they run, so they never see a computed out of step with its sources, and one that already re-ran for a direct change is not run a second time.
  • Bug fix: a computed that threw used to stay dirty, which stopped later changes of its sources from reaching its subscribers. It now retries on the next read and keeps propagating.
  • Bug fix: an effect invalidated by a computed while it was running could be left stale and never run again.
  • A computed that reads itself throws Cycle detected instead of recursing.

Component internals are stored on the instance (under a symbol) instead of a WeakMap, so every @Prop read is a property load instead of a map lookup. getComponentStart/setComponentStart are new in @praxisjs/core/internal.

getComponentAnchor() returns a Node instead of a Comment: a component that renders a single root node is anchored on that node, since @praxisjs/runtime no longer wraps it in comments. Its parentElement is the component's container as before. Every selector() now shares one bucket class instead of declaring one per call, so the graph code that links them always sees the same object shape.

Every effect records what was rendering when it was created (setCurrentOwner()), whether or not a reactive observer is installed, and effects created while an owned effect runs inherit its owner. getEffectOwner() reads it from the effect node instead of a WeakMap, so devtools can attribute effects created before profiling started.

@praxisjs/core/internal also exports the graph primitives the other packages build on: SignalNode, readSignal/writeSignal, ComputedNode/readComputed, createEffect/disposeEffect, createContextEffect (an effect whose shared function reads its state from this.ctx, so effects of one kind allocate no closure each), startBatch/endBatch and pauseTracking/resumeTracking.

One inert instance of each graph class (signal, computed, effect, link, selector bucket) is kept for the life of the page, so their V8 hidden classes, and the optimized code built on them, survive a full GC after every node of a kind was collected (a list cleared), instead of the next render running cold.

2.2.0

persistedSignal() now returns a close() method that removes its window storage listener. @Persisted calls it on unmount; calling it yourself is only needed for signals you create directly with syncTabs on. It also takes a new writeDelay option: writes to localStorage within the window are coalesced into one using the latest value, flushed on pagehide and on close(). A value arriving from another tab discards a still-pending local write. The default (0) keeps the previous write-on-every-set behaviour.

history() returns a destroy() method that stops its tracking effect; recorded entries stay readable. Every computed also has a dispose() that releases its subscriptions to its sources — it does nothing while anything still depends on it, so detaching can never leave a dependent stale.

batch() now deduplicates queued effects with a Set instead of scanning an array, so large batches no longer pay a quadratic cost.

Signals, computeds and effects now switch a subscriber list from an array to a Set once it holds more than 16 effects. Every time an effect re-runs it unsubscribes and resubscribes, and removing from a long array is O(n), so a signal read by thousands of bindings (a theme or locale signal in a large list) updated in quadratic time. In a browser benchmark, one signal driving 20,000 effects went from ~47 ms to ~1.6 ms per update. Subscription order is unchanged.

Bug fix: if an effect threw while a batch was flushing, the remaining effects were skipped and the queue was left with stale entries that re-ran on the next batch. The flush now detaches the queue first, runs every effect, and rethrows the last error afterwards — the same behaviour signal writes outside a batch already had.

2.1.0

Added a resource settle barrier and a server-render-pass flag for @praxisjs/ssg.

flushPendingResources() (from @praxisjs/core/internal) awaits every resource() fetch — keyed or not — started while a server render pass is active, including ones triggered by resources that themselves settle while waiting. setServerRenderPass() / isServerRenderPass() mark that pass; both are no-ops outside of it, so client-only apps are unaffected.

Also exposed trackPendingResource() from @praxisjs/core/internal, letting other framework packages register their own in-flight async work against the same barrier — @praxisjs/router uses it to track lazy route/layout resolution during a server render pass.

2.0.0

Breaking: component internals are no longer exposed as instance fields.

RootComponent now stores framework runtime state in private internal storage instead of public underscore fields. _rawProps, _mounted, _anchor, _setProps, _defaults, and _stateDirty are no longer part of component instances.

Framework packages use helper functions from @praxisjs/core/internal to read props, set anchors, update mount state, and mark state as dirty. App code should continue to use @Prop() on StatefulComponent classes; StatelessComponent<T> keeps its public props getter.

1.8.2

Internal: _setProps consolidated into RootComponent.

_setProps(props) was implemented identically in both StatefulComponent and StatelessComponent. It is now defined once on RootComponent and removed from the subclasses. No behavior change.

The framework-internal fields _rawProps, _mounted, _anchor, _defaults, and _stateDirty are now annotated with @internal JSDoc, hiding them from TypeDoc output and IDE autocomplete. No public API change.

1.8.1

Bug fix: dynamic reactive dependencies are cleaned up correctly.

Effects now unsubscribe from dependencies they no longer read when they rerun or stop. Computed values also drop stale branch subscriptions before recomputing, so conditional dependencies do not keep invalidating values after the active branch changes.

Signal notifications now iterate over a stable subscriber snapshot, ensuring that subscription changes during notification do not skip remaining listeners.

Bug fix: keyed resources keep cache and in-flight state consistent.

Calling mutate() on a resource with a key now writes the optimistic value into the shared cache, so future resources with the same key start from the mutated data. Settled keyed requests also clear their shared in-flight entry even if the local result was made stale by cancel().

1.8.0

onError can now return fallback DOM (error boundary).

onError(error: Error) now has return type Node | Node[] | null | void. When onError returns a node or array of nodes, the runtime mounts them in place of the failed component output. Return null or void to render nothing (previous behavior is unchanged).

onError(err: Error) {
  return <p class="error-fallback">Failed to load: {err.message}</p>
}

See Lifecycle Hooks — onError for full usage.


writableComputed(getter, setter) — new internal primitive.

Added writableComputed<T>(getter: () => T, setter: (value: T) => void): WritableComputed<T> to @praxisjs/core/internal. It wraps a computed() with set and update methods, returning a WritableComputed<T>. Used internally by @Computed({ set }) in @praxisjs/decorators.

Also added WritableComputed<T> type to @praxisjs/shared — extends Computed<T> with set(value: T): void and update(updater: (prev: T) => T): void.

1.7.0

resource() — cache, deduplication, stale-while-revalidate, refetch on focus.

New options on ResourceOptions:

  • key?: string — enables a global shared cache for this resource. All instances sharing the same key reuse in-flight requests (deduplication) and read from the same cache entry.
  • staleTime?: number — how long (ms) a cached entry is considered fresh. While fresh, the initial fetch is skipped. Default 0 means always stale: cached data is shown immediately while a background fetch runs (stale-while-revalidate).
  • refetchOnFocus?: boolean — when true, refetches whenever document.visibilityState becomes "visible". No-op in non-browser environments.

New Resource<T> method:

  • destroy() — stops reactive effects, removes the focus listener, and unregisters from the cache registry. Called automatically by the @Resource decorator on component unmount.

New export from @praxisjs/core/internal:

  • invalidateResource(key) — clears the cache entry for key and triggers an immediate refetch on every registered resource under that key.
  • _clearCache() — resets all cache state (tests only).

Internal: removed a redundant clearTimeout call from debounced() — the effect cleanup already handles it on every re-run.

1.6.0

Reactivity engine rewrite — lower allocation and cleaner internals. No API changes.

  • signal() — subscribers stored as null | Effect | Effect[], avoiding Set allocation for zero- and single-subscriber cases
  • computed() — plain closure, lazy recompute callback, separate tracking for downstream computeds vs leaf effects
  • effect() — simplified to a named closure with run and stop
  • batch() — pre-allocated module-level array instead of a new Set per call

1.5.0

New: syncedSignal(channelName, initialValue) — signal that stays in sync across browser tabs via BroadcastChannel. Writes in any tab broadcast to all others.

1.4.1

  • @Watch multi-prop coalescing — when multiple props change in the same tick, the callback fires once with the final values
  • Computed notification coalescing — leaf subscribers notified once per microtask boundary when multiple dependencies change simultaneously
  • TypeScript fix — createLifecycleMethodDecorator now accepts methods with typed parameters

1.4.0

StatelessComponent default type parameter changed from object to Record<never, never>. Components that only receive children no longer need a type argument.

1.3.0

StatelessComponent now exposes an optional typed children prop. this.props.children works without declaring it in T.

1.2.0

New: peek(signal) and untrack(fn) — promoted to the public API.

Bug fix: mountComponent now runs inside untrack(). Static reads like description={this.count} inside a reactive context (e.g. the router) were accidentally subscribing the outer effect to those signals, causing the component to re-mount on every state change.

1.1.0

  • debounced() returns a .stop() method to cancel the pending timer
  • Nested batch() calls no longer overwrite the outer queue
  • Subscriber errors are now isolated — all subscribers run even when one throws

1.0.0

Breaking

All functional APIs replaced by decorators across the entire ecosystem.

  • Added Composable abstract base class for class-based composables
  • Removed resource, createResource, Resource, ResourceStatus, ResourceOptions — use @Resource from @praxisjs/decorators instead

0.4.2

Bug fix: effect() stop function now prevents future re-runs by setting a stopped flag. Previously it only ran cleanup but left the effect subscribed.

0.4.1

Bug fix: when() no longer leaks its reactive effect when the source is immediately truthy on the first run.

0.4.0

Added @Computed() decorator for cached reactive getters.

0.3.0

Migrated from legacy decorator signatures to the TC39 decorator context API. Introduced StatefulComponent and StatelessComponent.

0.1.0

Initial beta release.

On this page