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