JavaScript Decorators: Memoize, Log, and Deprecated for WordPress JS

JavaScript Decorators (TC39 Stage 3, shipping in Babel 7.21+ and TypeScript 5.0+) are a meta-programming feature that lets you annotate and modify classes, methods, and accessors declaratively. For WordPress JavaScript development they are most useful for memoising expensive computations, logging Gutenberg store actions, and marking deprecated APIs.

Problem: WordPress admin JavaScript tools — data tables, export utilities, analytics dashboards — contain repetitive cross-cutting logic: logging function calls, caching expensive results, and marking deprecated APIs. This logic is duplicated across modules with no consistent mechanism.

Solution: Use JavaScript Decorators (stage 3) to implement these cross-cutting concerns: a @memoize decorator caches return values by arguments, a @log decorator records calls to a debug panel, and a @deprecated decorator emits a console warning. Apply them declaratively to class methods with no changes to the method body.

The examples below implement a @memoize decorator for block editor selector functions, a @log method decorator for debugging WP data store actions, and a @deprecated decorator that mirrors PHP's #[Deprecated] attribute.

// Decorators need Babel with @babel/plugin-proposal-decorators
// or TypeScript 5.0+ with experimentalDecorators: true (legacy)
// or the new TC39 decorators (--experimentalDecorators not needed in TS 5.2+)

// ── @memoize — cache method results by serialised arguments ──
function memoize(target, context) {
    // context.kind === 'method'
    const cache = new Map();

    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        const result = target.call(this, ...args);
        cache.set(key, result);
        return result;
    };
}

class PostSelector {
    @memoize
    getPostsByCategory(categoryId) {
        // Expensive computation — only runs once per unique categoryId
        return wp.data.select('core').getEntityRecords('postType', 'post', {
            categories: [categoryId],
            per_page: 100,
        });
    }
}

// ── @readonly — prevent method reassignment ──
function readonly(target, context) {
    return function(...args) {
        return target.call(this, ...args);
    };
    // In TC39 decorators, set descriptor.writable = false for fields
}

// ── @log — trace method calls with timing ──
function log(target, context) {
    const name = context.name;
    return function(...args) {
        console.time(`${name}()`);
        try {
            return target.apply(this, args);
        } finally {
            console.timeEnd(`${name}()`);
        }
    };
}

Class and accessor decorators for WordPress data stores:

// ── @deprecated — warn on usage ──
function deprecated(message) {
    return function(target, context) {
        return function(...args) {
            console.warn(`[Deprecated] ${String(context.name)}: ${message}`);
            return target.apply(this, args);
        };
    };
}

// ── @bound — auto-bind method to instance (replaces constructor binding) ──
function bound(target, context) {
    context.addInitializer(function() {
        this[context.name] = target.bind(this);
    });
}

// ── Usage in a Gutenberg block component ──
class BlockToolbar extends React.Component {
    @bound
    handleClick(event) {
        // `this` is always the component instance — no .bind(this) needed
        this.setState({ active: !this.state.active });
    }

    @deprecated('Use getPostsByStatus() instead')
    getPublishedPosts() {
        return this.getPostsByStatus('publish');
    }

    @memoize
    computeAttributes(rawData) {
        // Heavy transformation — cached after first call
        return rawData.map(item => ({ ...item, processed: true }));
    }

    render() {
        return ;
    }
}

// ── Auto-accessor decorator (TC39 stage 3 only) ──
class Store {
    @log
    accessor currentPostId = null;
    // Generates get/set backed by a private slot, with @log wrapping both
}

NOTE: Configure Babel for TC39 Stage 3 decorators in a WordPress block plugin by adding {"plugins": [["@babel/plugin-proposal-decorators", {"version": "2023-11"}]]} to babel.config.json. The new TC39 decorators are NOT compatible with TypeScript's legacy experimentalDecorators — if upgrading an existing codebase, migrate one file at a time using --experimentalDecorators alongside the new syntax only after separating the files.

Leave Comment

Your email address will not be published. Required fields are marked *