The TC39 Signals proposal (Stage 1 as of 2025) brings a standardised reactive primitive to JavaScript — the same mental model behind Angular signals, Preact signals, Solid.js, and Vue’s reactivity system. A Signal holds a value; any computation that reads it is automatically re-run when the value changes. The @tc39/signal-polyfill package lets you experiment with the API today.
Problem: JavaScript state management in WordPress block editor extensions or admin SPAs relies on Redux-style stores from @wordpress/data — which requires boilerplate actions, reducers, and selectors even for simple reactive values shared between components.
Solution: Use JavaScript Signals (TC39 stage 1, or libraries like @preact/signals compatible with WordPress's Preact build) for fine-grained reactivity without a store. A signal is a reactive value: read it anywhere, mutate it, and all dependents re-render automatically. Computed signals memoize derived values without explicit selectors.
The examples below show basic signal creation, computed signals, effects (side-effects that re-run on change), and a practical WordPress admin pattern: a reactive cart counter that updates a DOM badge whenever the WooCommerce Store API cart changes.
// ── Using the TC39 signal polyfill ───────────────────────────────────────
import { Signal } from '@tc39/signal-polyfill';
import { effect } from '@preact/signals-core'; // or a custom effect scheduler
// 1. Basic signal
const count = new Signal.State( 0 );
console.log( count.get() ); // 0
count.set( 5 );
console.log( count.get() ); // 5
// 2. Computed signal — derived, cached, lazy
const doubled = new Signal.Computed( () => count.get() * 2 );
console.log( doubled.get() ); // 10
count.set( 7 );
console.log( doubled.get() ); // 14 (recomputed only when read after change)
// 3. Multiple dependent signals
const firstName = new Signal.State( 'John' );
const lastName = new Signal.State( 'Doe' );
const fullName = new Signal.Computed( () => `${firstName.get()} ${lastName.get()}` );
firstName.set( 'Jane' );
console.log( fullName.get() ); // "Jane Doe"
// 4. Practical: reactive WooCommerce cart badge
// Assume this runs in a block editor / FSE context with Store API available
const cartCount = new Signal.State( 0 );
// Fetch initial cart
fetch( '/wp-json/wc/store/v1/cart', { credentials: 'same-origin' } )
.then( r => r.json() )
.then( data => cartCount.set( data.items_count ?? 0 ) );
// Subscribe to Store API cart updates (WooCommerce block cart store)
if ( window.wc?.wcStore ) {
window.wc.wcStore.subscribe( () => {
const state = window.wc.wcStore.getState();
const count = state?.cart?.itemsCount ?? 0;
cartCount.set( count );
} );
}
// Effect: update the DOM badge whenever cartCount changes
// (using a minimal effect wrapper around the Watcher API)
function watchSignal( signal, callback ) {
const watcher = new Signal.subtle.Watcher( () => {
watcher.watch(); // re-arm
callback( signal.get() );
} );
watcher.watch( signal );
callback( signal.get() ); // run once immediately
}
const badge = document.querySelector( '.cart-count-badge' );
if ( badge ) {
watchSignal( cartCount, value => {
badge.textContent = value;
badge.hidden = value === 0;
} );
}
NOTE: The TC39 Signals API is still a proposal and the polyfill API may change; for production use today, prefer the framework-specific signal libraries (@preact/signals-core, @angular/core signals, or Vue's ref()/computed()) which are stable and ship their own efficient effect schedulers.