Skill 09 · Wp Interactivity API
Subchapter 9.3
references/server-side-rendering.mdMarkdown8 KBView on GitHub
For components using block.json:
{
"supports": {
"interactivity": true
}
}For themes/plugins without block.json:
Use wp_interactivity_process_directives() to manually process directives (see “Themes and Plugins without block.json” section below).
Define initial state values in PHP before rendering:
wp_interactivity_state( 'myPlugin', array(
'fruits' => array( 'Apple', 'Banana', 'Cherry' ),
'isLoading' => false,
'count' => 3,
));The state is serialized and available to client JavaScript automatically.
For element-scoped context:
<?php
$context = array(
'isOpen' => false,
'itemId' => 42,
'itemName' => 'Example',
);
?>
<div
data-wp-interactive="myPlugin"
<?php echo wp_interactivity_data_wp_context( $context ); ?>
>
<button data-wp-on-async--click="actions.toggle">
Toggle
</button>
<div data-wp-bind--hidden="!context.isOpen">
Content for <?php echo esc_html( $context['itemName'] ); ?>
</div>
</div>When derived state affects the initial HTML, define it in PHP to avoid layout shifts.
When the derived value is known at render time:
$fruits = array( 'Apple', 'Banana', 'Cherry' );
$hasFruits = count( $fruits ) > 0;
wp_interactivity_state( 'myPlugin', array(
'fruits' => $fruits,
'hasFruits' => $hasFruits,
));When the value depends on context (e.g., inside data-wp-each loops):
wp_interactivity_state( 'myPlugin', array(
'fruits' => array( 'apple', 'banana', 'cherry' ),
'shoppingList' => array( 'apple', 'cherry' ),
'onShoppingList' => function() {
$state = wp_interactivity_state();
$context = wp_interactivity_get_context();
return in_array( $context['item'], $state['shoppingList'] ) ? 'Yes' : 'No';
},
));The closure is evaluated during directive processing for each element.
<?php
$fruits = array( 'Apple', 'Banana', 'Cherry' );
wp_interactivity_state( 'myFruitPlugin', array(
'fruits' => $fruits,
'hasFruits' => count( $fruits ) > 0,
'mango' => __( 'Mango' ),
));
?>
<div data-wp-interactive="myFruitPlugin">
<button data-wp-on-async--click="actions.addMango">
<?php esc_html_e( 'Add Mango' ); ?>
</button>
<button data-wp-on-async--click="actions.clearAll">
<?php esc_html_e( 'Clear All' ); ?>
</button>
<ul data-wp-bind--hidden="!state.hasFruits">
<template data-wp-each="state.fruits">
<li data-wp-text="context.item"></li>
</template>
</ul>
<p data-wp-bind--hidden="state.hasFruits">
<?php esc_html_e( 'No fruits available.' ); ?>
</p>
</div>import { store, getContext } from '@wordpress/interactivity';
const { state } = store( 'myFruitPlugin', {
state: {
get hasFruits() {
return state.fruits.length > 0;
},
},
actions: {
addMango() {
state.fruits.push( state.mango );
},
clearAll() {
state.fruits = [];
},
},
});<div data-wp-interactive="myFruitPlugin">
<button data-wp-on-async--click="actions.addMango">Add Mango</button>
<button data-wp-on-async--click="actions.clearAll">Clear All</button>
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</ul>
<p hidden>No fruits available.</p>
</div>The hidden attribute is added server-side because state.hasFruits is true.
Use wp_interactivity_state() to pass server values to client JavaScript:
wp_interactivity_state( 'myPlugin', array(
'labels' => array(
'add' => __( 'Add Item', 'textdomain' ),
'remove' => __( 'Remove Item', 'textdomain' ),
'empty' => __( 'No items found', 'textdomain' ),
),
));wp_interactivity_state( 'myPlugin', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'myPlugin_nonce' ),
'restUrl' => rest_url( 'myPlugin/v1/' ),
));const { state } = store( 'myPlugin', {
actions: {
*fetchData() {
const formData = new FormData();
formData.append( 'action', 'my_action' );
formData.append( '_ajax_nonce', state.nonce );
const response = yield fetch( state.ajaxUrl, {
method: 'POST',
body: formData,
});
return yield response.json();
},
},
});For themes or plugins not using block.json, use wp_interactivity_process_directives():
<?php
wp_interactivity_state( 'myTheme', array(
'menuOpen' => false,
));
ob_start();
?>
<nav
data-wp-interactive="myTheme"
data-wp-class--is-open="state.menuOpen"
>
<button data-wp-on-async--click="actions.toggleMenu">
Menu
</button>
<ul data-wp-bind--hidden="!state.menuOpen">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<?php
$html = ob_get_clean();
echo wp_interactivity_process_directives( $html );Only call wp_interactivity_process_directives() once at the outermost template level.
| Function | Purpose |
|---|---|
wp_interactivity_state( $namespace, $state ) | Initialize/get global state for a namespace |
wp_interactivity_data_wp_context( $context ) | Generate data-wp-context attribute |
wp_interactivity_get_context( $namespace ) | Get current context during directive processing |
wp_interactivity_process_directives( $html ) | Manually process directives (themes/plugins) |
For block.json users: Without supports.interactivity, directives are not processed:
{
"supports": {
"interactivity": true
}
}For themes/plugins: Ensure wp_interactivity_process_directives() is called on the HTML output.
If state.hasFruits is only defined in JavaScript, the hidden attribute won’t be set:
<!-- Without server state: shows briefly then hides (layout shift) -->
<p data-wp-bind--hidden="state.hasFruits">No fruits</p>Ensure PHP and JavaScript derived state logic matches:
// PHP
'hasFruits' => count( $fruits ) > 0,// JavaScript - must match PHP logic
get hasFruits() {
return state.fruits.length > 0;
}