Building a Reactive Frontend Framework inside Full Site Editing (FSE) Block Themes and theme.json Using Custom Action and Filter Hooks
Leveraging `theme.json` and Custom Hooks for Reactive FSE Components
Full Site Editing (FSE) in WordPress, powered by block themes and the `theme.json` configuration file, offers a robust foundation for building dynamic and customizable websites. However, achieving truly reactive frontend behavior—where UI elements update instantly based on data changes or user interactions without full page reloads—often requires going beyond the standard block API. This post details how to architect a reactive frontend component system within the FSE paradigm by strategically employing custom action and filter hooks, and by deeply integrating with `theme.json` for dynamic styling and configuration.
Defining Reactive Component State and Actions
The core of any reactive system lies in managing state and defining how that state can be mutated. In a WordPress context, we can abstract this by creating custom JavaScript classes or objects that hold component state and expose methods for updating it. These methods will be the “actions” that trigger UI re-renders. For this example, let’s imagine a simple “Live Counter” component that increments or decrements a value displayed on the frontend, with its initial value and step controlled via `theme.json`.
JavaScript State Management and Actions
We’ll define a JavaScript class to encapsulate the counter’s logic. This class will hold the current count and provide methods to modify it. Crucially, it will also dispatch custom events that our frontend rendering logic can listen to.
class LiveCounter {
constructor(initialValue = 0, step = 1) {
this.count = initialValue;
this.step = step;
this.listeners = [];
this.dispatchUpdate(); // Initial dispatch
}
// Method to subscribe to state changes
subscribe(callback) {
this.listeners.push(callback);
// Immediately call back with current state
callback(this.count);
}
// Method to unsubscribe
unsubscribe(callback) {
this.listeners = this.listeners.filter(listener => listener !== callback);
}
// Dispatch an update event to all subscribers
dispatchUpdate() {
this.listeners.forEach(listener => {
try {
listener(this.count);
} catch (error) {
console.error("Error in listener callback:", error);
}
});
}
// Action: Increment the counter
increment() {
this.count += this.step;
this.dispatchUpdate();
}
// Action: Decrement the counter
decrement() {
this.count -= this.step;
this.dispatchUpdate();
}
// Action: Set a specific value
set(value) {
this.count = value;
this.dispatchUpdate();
}
// Get current count
getCount() {
return this.count;
}
// Get current step
getStep() {
return this.step;
}
}
Integrating with `theme.json` for Configuration
The power of FSE lies in `theme.json`, which acts as a central configuration hub for styles and settings. We can expose custom settings within `theme.json` that our JavaScript components can read. This allows theme developers to control component behavior and appearance directly from the WordPress Customizer or block editor interface, without touching code.
Defining Custom Settings in `theme.json`
We’ll add a custom section to `theme.json` to define the initial value and step for our `LiveCounter`. These values will be accessible via the WordPress REST API and can be enqueued into our JavaScript.
{
"version": 2,
"settings": {
"color": {
// ... existing color settings
},
"layout": {
// ... existing layout settings
},
"custom": {
"liveCounter": {
"initialValue": 10,
"step": 5,
"textColor": "#333333",
"backgroundColor": "#f0f0f0"
}
}
},
"styles": {
// ... existing styles
}
}
Enqueuing `theme.json` Data into JavaScript
To make the `theme.json` settings available to our frontend JavaScript, we need to enqueue them. This is typically done using `wp_localize_script` or by directly embedding JSON data into a script tag. A common and clean approach is to use a custom REST API endpoint or to filter the `wp_get_global_settings()` output.
Using `wp_get_global_settings()` and `wp_add_inline_script`
We can hook into `wp_get_global_settings()` to augment the data passed to the frontend. Then, we’ll use `wp_add_inline_script` to inject this data into our JavaScript bundle.
/**
* Enqueue custom scripts and localize theme settings.
*/
function my_theme_enqueue_scripts() {
// Register and enqueue your main JavaScript file
wp_enqueue_script(
'my-reactive-frontend',
get_template_directory_uri() . '/assets/js/reactive-frontend.js',
array( 'wp-element', 'wp-blocks', 'wp-components', 'wp-i18n' ), // Dependencies
filemtime( get_template_directory() . '/assets/js/reactive-frontend.js' ),
true // Load in footer
);
// Get global settings and add our custom data
$global_settings = wp_get_global_settings();
$custom_settings = isset( $global_settings['custom'] ) ? $global_settings['custom'] : [];
// Ensure our specific settings exist, with fallbacks
$live_counter_settings = wp_parse_args( $custom_settings['liveCounter'] ?? [], [
'initialValue' => 0,
'step' => 1,
'textColor' => '#000000',
'backgroundColor' => '#ffffff',
] );
// Localize the script with our settings
wp_localize_script(
'my-reactive-frontend',
'themeSettings',
array(
'liveCounter' => $live_counter_settings,
'ajaxUrl' => admin_url( 'admin-ajax.php' ), // Useful for future AJAX actions
)
);
}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_scripts' );
/**
* Filter global settings to include custom values.
* This is an alternative/complementary approach to wp_localize_script
* if you want the data available globally in JS without a specific handle.
*/
function my_theme_filter_global_settings( $settings ) {
if ( ! isset( $settings['custom']['liveCounter'] ) ) {
// Fallback if not already defined or for development
$settings['custom']['liveCounter'] = [
'initialValue' => 0,
'step' => 1,
'textColor' => '#000000',
'backgroundColor' => '#ffffff',
];
}
return $settings;
}
// Note: This filter is often applied in the block editor context or for global JS variables.
// For frontend scripts, wp_localize_script is generally preferred for clarity.
// add_filter( 'option_wp_global_settings', 'my_theme_filter_global_settings' );
Implementing Frontend Rendering and Reactivity
With our JavaScript state manager and configuration in place, we can now build the frontend component. This involves creating a DOM element, attaching event listeners to our `LiveCounter` instance, and updating the DOM whenever the state changes.
DOM Manipulation and Event Binding
We’ll create a JavaScript file (e.g., `reactive-frontend.js`) that initializes our `LiveCounter` and renders the UI. The rendering function will be called initially and then again every time the `LiveCounter` state updates.
// Assuming themeSettings is available globally from wp_localize_script
// and LiveCounter class is defined in the same scope or imported.
// Get settings from localized script
const liveCounterConfig = themeSettings.liveCounter || {
initialValue: 0,
step: 1,
textColor: '#000000',
backgroundColor: '#ffffff'
};
// Instantiate the LiveCounter
const counterInstance = new LiveCounter(liveCounterConfig.initialValue, liveCounterConfig.step);
// --- Rendering Logic ---
const renderCounter = (currentCount) => {
// Find or create the main counter element
let counterElement = document.getElementById('live-counter-display');
if (!counterElement) {
counterElement = document.createElement('div');
counterElement.id = 'live-counter-display';
counterElement.style.padding = '20px';
counterElement.style.border = '1px solid #ccc';
counterElement.style.textAlign = 'center';
counterElement.style.fontFamily = 'sans-serif';
counterElement.style.color = liveCounterConfig.textColor;
counterElement.style.backgroundColor = liveCounterConfig.backgroundColor;
document.body.appendChild(counterElement); // Append to body or a specific container
}
// Update the display
counterElement.innerHTML = `
<h3>Live Counter</h3>
<p style="font-size: 2em; margin: 10px 0;">${currentCount}</p>
<button id="increment-btn" style="margin: 5px; padding: 10px;">+</button>
<button id="decrement-btn" style="margin: 5px; padding: 10px;">-</button>
`;
// Re-attach event listeners after rendering to ensure they exist
attachEventListeners();
};
// --- Event Binding ---
const attachEventListeners = () => {
const incrementButton = document.getElementById('increment-btn');
const decrementButton = document.getElementById('decrement-btn');
if (incrementButton) {
// Remove existing listeners to prevent duplicates if render is called multiple times
incrementButton.removeEventListener('click', handleIncrement);
incrementButton.addEventListener('click', handleIncrement);
}
if (decrementButton) {
decrementButton.removeEventListener('click', handleDecrement);
decrementButton.addEventListener('click', handleDecrement);
}
};
// Event Handlers
const handleIncrement = () => {
counterInstance.increment();
};
const handleDecrement = () => {
counterInstance.decrement();
};
// --- Subscription ---
// Subscribe to state changes and re-render
counterInstance.subscribe(renderCounter);
// Initial render is handled by the subscription's initial call.
// If you need to render before the subscription is active, call renderCounter directly:
// renderCounter(counterInstance.getCount());
Advanced: Custom Action and Filter Hooks for Extensibility
To make our reactive system truly extensible and integrate seamlessly with the WordPress ecosystem, we can define custom action and filter hooks. This allows other plugins or theme files to hook into our component’s lifecycle or modify its behavior.
Defining Custom PHP Hooks
We can define PHP hooks within our theme’s `functions.php` or a dedicated plugin. These hooks can be fired at various stages, such as before initialization, after state changes, or when rendering.
/** * Fires before the LiveCounter component is initialized. * * @param array $config The configuration array for the LiveCounter. */ do_action( 'my_theme_live_counter_before_init', $live_counter_settings ); /** * Fires after the LiveCounter component's state has been updated. * * @param int $new_count The new count value. * @param int $step The step value used for the update. */ do_action( 'my_theme_live_counter_after_update', $new_count, $step ); /** * Filters the HTML output of the LiveCounter component. * * @param string $html The generated HTML for the counter. * @param int $current_count The current count value. */ $rendered_html = apply_filters( 'my_theme_live_counter_render_html', $rendered_html, $current_count );
Hooking into JavaScript Events
Similarly, we can dispatch custom JavaScript events that can be caught by other scripts. This is a more idiomatic JavaScript approach to extensibility.
// Inside the LiveCounter class, after dispatchUpdate:
dispatchUpdate() {
// Dispatch a custom DOM event
const updateEvent = new CustomEvent('live-counter-update', {
detail: { count: this.count, step: this.step }
});
document.dispatchEvent(updateEvent); // Dispatch globally or to a specific element
// ... existing listener dispatch ...
}
// In reactive-frontend.js or another script:
document.addEventListener('live-counter-update', (event) => {
console.log('Live counter updated via DOM event:', event.detail);
// Perform additional actions based on the update
// For example, update another UI element, trigger an AJAX call, etc.
});
// Example of hooking into the rendering process
const originalRenderCounter = renderCounter;
renderCounter = (currentCount) => {
let html = `
<h3>Live Counter</h3>
<p style="font-size: 2em; margin: 10px 0;">${currentCount}</p>
<button id="increment-btn" style="margin: 5px; padding: 10px;">+</button>
<button id="decrement-btn" style="margin: 5px; padding: 10px;">-</button>
`;
// Apply filters if they exist (simulated)
if (typeof wp_apply_filters !== 'undefined') { // Assuming a WP-like filter system in JS
html = wp_apply_filters('my_theme_live_counter_render_html', html, currentCount);
}
originalRenderCounter(currentCount); // Call the original rendering logic
};
Advanced Diagnostics and Debugging
When building complex reactive systems, debugging can become challenging. Here are some advanced diagnostic techniques:
1. State Inspector in Browser DevTools
Leverage the browser’s developer console extensively. You can:
- Log state changes within the `LiveCounter` class’s `dispatchUpdate` method.
- Use `console.table()` to inspect arrays or objects passed to listeners.
- Set breakpoints in your rendering logic and event handlers to step through execution.
- Inspect the `themeSettings` object in the console to verify that `theme.json` data is loaded correctly.
// Example: Logging state changes in LiveCounter
class LiveCounter {
// ... constructor and other methods ...
dispatchUpdate() {
console.log('LiveCounter state update:', { count: this.count, step: this.step }); // Log state
this.listeners.forEach(listener => {
try {
listener(this.count);
} catch (error) {
console.error("Error in listener callback:", error);
}
});
}
// ...
}
2. WordPress Debugging Tools
Ensure `WP_DEBUG` and `WP_DEBUG_LOG` are enabled in your `wp-config.php` for PHP errors. For JavaScript, browser console logs are primary. You can also use PHP’s `error_log()` to send messages to the WordPress debug log from your PHP hooks.
// In your PHP hook, for example:
function my_theme_live_counter_after_update_handler( $new_count, $step ) {
error_log( sprintf( 'Live counter updated. New count: %d, Step: %d', $new_count, $step ) );
// You can also log the result of apply_filters here
}
add_action( 'my_theme_live_counter_after_update', 'my_theme_live_counter_after_update_handler', 10, 2 );
3. Network Tab Analysis
Monitor the Network tab in your browser’s dev tools to check:
- If your JavaScript files are loading correctly (status 200).
- If any AJAX requests (if you implement them later) are failing (status 4xx, 5xx).
- The response from `admin-ajax.php` or REST API endpoints if used for dynamic data fetching.
4. Inspecting `theme.json` Loading
Verify that `theme.json` is being parsed and its values are accessible. You can do this by:
- Checking the `themeSettings` object in the JavaScript console.
- Using `wp_debug_backtrace_summary()` in PHP to trace where `wp_get_global_settings()` is being called and what data it contains.
- In the browser, you can often inspect the global `wp` object or specific localized script variables.
Conclusion
By combining custom JavaScript state management, strategic use of `theme.json` for configuration, and the power of custom action/filter hooks (both in PHP and JavaScript), you can build sophisticated, reactive frontend components within WordPress’s FSE framework. This approach not only enhances user experience through dynamic UIs but also provides a robust and extensible architecture that adheres to WordPress best practices.