Recipe: WordPress + Micra.js
WordPress already renders your HTML on the server. Micra adds reactivity to the
few spots that need it — a like button, a filterable archive, an AJAX form —
without a block-build toolchain, @wordpress/scripts, or a single line of
React. You enqueue one script and sprinkle data-component onto the markup PHP
already prints.
1. Enqueue Micra
Load the library from a CDN (or a local copy) in functions.php. Enqueue it in
the footer so the DOM exists when it runs, and make your component file depend
on it so order is guaranteed.
add_action('wp_enqueue_scripts', function () {
wp_enqueue_script(
'micra',
'https://cdn.jsdelivr.net/npm/micra.js/dist/micra.min.js',
[], // no dependencies
null, // no version query
true // load in footer
);
// your component definitions, after micra
wp_enqueue_script(
'site-components',
get_stylesheet_directory_uri() . '/js/components.js',
['micra'], // depends on micra
null,
true
);
});
2. Sprinkle a component with a shortcode
A shortcode is the cleanest way to drop a component into a post, page, or
template. Server data goes onto data-* attributes — Micra reads them with
this.prop(), JSON-parsing automatically.
add_shortcode('like_button', function ($atts) {
$atts = shortcode_atts(['post' => get_the_ID(), 'count' => 0], $atts);
ob_start(); ?>
<div
data-component="like-button"
data-post="<?php echo esc_attr($atts['post']); ?>"
data-count="<?php echo esc_attr($atts['count']); ?>"
data-nonce="<?php echo esc_attr(wp_create_nonce('wp_rest')); ?>"
>
<button @click="like" data-bind="disabled:liked">
<span data-text="liked ? 'Liked' : 'Like'"></span>
<span data-text="'(' + count + ')'"></span>
</button>
</div>
<?php
return ob_get_clean();
});
Drop [like_button] into any post, or call echo do_shortcode('[like_button]')
in a template (single.php, a block, an Elementor HTML widget — anywhere).
3. Define the component
In your theme’s js/components.js (no build step — plain JS using the global
Micra):
Micra.define("like-button", {
state: { count: 0, liked: false, post: 0, nonce: "" },
onCreate() {
this.state.count = this.prop("count", 0);
this.state.post = this.prop("post");
this.state.nonce = this.prop("nonce");
},
like() {
if (this.state.liked) return;
this.state.liked = true;
this.state.count = this.state.count + 1; // optimistic
this.fetch("/wp-json/my-plugin/v1/like", {
method: "POST",
headers: { "X-WP-Nonce": this.state.nonce },
body: { post: this.state.post },
});
},
});
Micra.start(); // mounts every [data-component] on the page
this.prop() reads the data-* attributes and casts them — data-count="3"
becomes the number 3, data-post the post ID. The WordPress REST nonce rides
in the X-WP-Nonce header so the request is authenticated.
4. Server-rendered lists stay server-rendered
Don’t rebuild the loop in JavaScript. Let PHP print the rows it already knows
how to print, then let Micra filter them client-side with data-each over data
you hand it — or keep the markup and just toggle visibility. For an archive
filter, pass the posts as a JSON prop:
<div data-component="post-filter" data-posts='<?php echo esc_attr(wp_json_encode($posts)); ?>'>
<input data-model="query" placeholder="Filter posts…" />
<ul>
<template data-each="filtered()" data-key="id">
<li><a data-bind="href:item.link" data-text="item.title"></a></li>
</template>
</ul>
</div>
Micra.define("post-filter", {
state: { query: "", posts: [] },
onCreate() { this.state.posts = this.prop("posts"); },
filtered() {
const q = this.state.query.trim().toLowerCase();
return this.state.posts.filter((p) => p.title.toLowerCase().includes(q));
},
});
Gotchas
- Enqueue in the footer (
in_footer = true) so the elements exist beforeMicra.start()runs. If you must load in<head>, wrap the start call inDOMContentLoaded. - Block editor vs front end. Components run on the front end. Inside the
block editor canvas they won’t (and shouldn’t) mount — render a static preview
for the editor and the live component on
the_content. - Use the REST nonce for writes.
wp_create_nonce('wp_rest')on the server,X-WP-Nonceheader on the request. Reads of public data need no nonce. - One script, no build. There’s no
npm run build, no JSX, no block.json — the whole point. If you’re reaching for@wordpress/scripts, you’ve left the recipe.