The Interactivity API offers a powerful framework for creating interactive blocks. To make the most of its capabilities, it’s crucial to understand when to use global state, local context, or derived state. This guide will clarify these concepts and provide practical examples to help you decide when to use each one.
Let’s start with a brief definition of global state, local context and derived state.
- Global state: Global data that can be accessed and modified by any interactive block on the page, allowing different parts of your interactive blocks to stay in sync.
- Local context: Local data defined within a specific element in the HTML structure, accessible only to that element and its children, providing independent state for individual blocks.
- Derived state: Computed values based on global state or local context, dynamically calculated on-demand to ensure consistent data representation without storing redundant data.
Let’s now dive into each of these concepts to study them in more detail and provide some examples.
Global state
Global state in the Interactivity API refers to global data that can be accessed and modified by any interactive block on the page. It serves as a shared information hub, allowing different parts of your blocks to communicate and stay in sync. Global state is the ideal mechanism for exchanging information between interactive blocks, regardless of their position in the DOM tree.
You should use global state when:
- You need to share data between multiple interactive blocks that are not directly related in the DOM hierarchy.
- You want to maintain a single source of truth for certain data across all your interactive blocks.
- You’re dealing with data that affects multiple parts of your UI simultaneously.
- You want to implement features that are global for the page.
Working with global state
- Initializing the global state
Typically, the initial global state values should be defined on the server using the
wp_interactivity_state
function:// Populates the initial global state values. wp_interactivity_state( 'myPlugin', array( 'isDarkTheme' => true, 'show' => false, 'helloText' => __( 'world' ), ));
These initial global state values will be used during the rendering of the page in PHP to populate the HTML markup that is sent to the browser.
- HTML markup written in the PHP file by the developer:
<div data-wp-interactive="myPlugin" data-wp-class--is-dark-theme="state.isDarkTheme" class="my-plugin" > <div data-wp-bind--hidden="!state.show"> Hello <span data-wp-text="state.helloText"></span> </div> <button data-wp-on-async--click="actions.toggle">Toggle</button> </div>
- HTML markup after the directives have been processed and it is ready to be sent to the browser:
<div data-wp-interactive="myPlugin" data-wp-class--is-dark-theme="state.isDarkTheme" class="my-plugin is-dark-theme" > <div hidden data-wp-bind--hidden="!state.show"> Hello <span data-wp-text="state.helloText">world</span> </div> <button data-wp-on-async--click="actions.toggle">Toggle</button> </div>
Please, visit the Server-side Rendering guide to learn more about how directives are processed on the server.
In cases where the global state is not used during the rendering of the page in PHP, it can also be defined directly on the client.
const { state } = store( 'myPlugin', { state: { isLoading: false, }, actions: { *loadSomething() { state.isLoading = true; // ... }, }, } );
Please note that, although this works, in general it is a good practice to define all the global state on the server.
- HTML markup written in the PHP file by the developer:
-
Accessing the global state
In the HTML markup, you can access the global state values directly by referencing
state
in the directive attribute values:<div data-wp-bind--hidden="!state.show"> <span data-wp-text="state.helloText"></span> </div>
In JavaScript, the
store
function from the package at@wordpress/interactivity
works both as a setter and a getter, returning the store of the selected namespace.To access the global state in your actions and callbacks, you can use the
state
property of the object returned by thestore
function:const myPluginStore = store( 'myPlugin' ); myPluginStore.state; // This is the state of the 'myPlugin' namespace.
You can also destructure the object returned by
store
:const { state } = store( 'myPlugin' );
And you can do the same even if you are defining the store at that moment, which is the most common scenario:
const { state } = store( 'myPlugin', { state: { // ... }, actions: { toggle() { state.show = ! state.show; }, }, } );
The global state initialized on the server using the
wp_interactivity_state
function is also included in that object because it is automatically serialized from the server to the client:wp_interactivity_state( 'myPlugin', array( 'someValue' => 1, ));
const { state } = store( 'myPlugin', { state: { otherValue: 2, }, actions: { readGlobalState() { state.someValue; // It exists and its initial value is 1. state.otherValue; // It exists and its initial value is 2. }, }, } );
Lastly, all calls to the
store
function with the same namespace are merged together:store( 'myPlugin', { state: { someValue: 1 } } ); store( 'myPlugin', { state: { otherValue: 2 } } ); /* All calls to `store` return a stable reference to the same object, so you * can get a reference to `state` from any of them. */ const { state } = store( 'myPlugin' ); store( 'myPlugin', { actions: { readValues() { state.someValue; // It exists and its initial value is 1. state.otherValue; // It exists and its initial value is 2. }, }, } );
- Updating the global state
To update the global state, all you need to do is mutate the
state
object once you have obtained it from thestore
function:const { state } = store( 'myPlugin', { actions: { updateValues() { state.someValue = 3; state.otherValue = 4; }, }, } );
Changes to the global state will automatically trigger updates in any directives that depend on the modified values.
Please, visit The Reactive and Declarative mindset guide to learn more about how reactivity works in the Interactivity API.
Example: Two interactive blocks using global state to communicate
In this example, there are two independent interactive blocks. One displays a counter, and the other a button to increment that counter. These blocks can be positioned anywhere on the page, regardless of the HTML structure. In other words, one does not need to be an inner block of the other.
- Counter Block
<?php wp_interactivity_state( 'myCounterPlugin', array( 'counter' => 0 )); ?> <div data-wp-interactive="myCounterPlugin" <?php echo get_block_wrapper_attributes(); ?> > Counter: <span data-wp-text="state.counter"></span> </div>
- Increment Block
<div data-wp-interactive="myCounterPlugin" <?php echo get_block_wrapper_attributes(); ?> > <button data-wp-on-async--click="actions.increment"> Increment </button> </div>
const { state } = store( 'myCounterPlugin', { actions: { increment() { state.counter += 1; }, }, } );
In this example:
- The global state is initialized on the server using
wp_interactivity_state
, setting an initialcounter
of 0. - The Counter Block displays the current counter using
data-wp-text="state.counter"
, which reads from the global state. - The Increment Block contains a button that triggers the
increment
action when clicked, usingdata-wp-on-async--click="actions.increment"
. - In JavaScript, the
increment
action directly modifies the global state by incrementingstate.counter
.
Both blocks are independent and can be placed anywhere on the page. They don’t need to be nested or directly related in the DOM structure. Multiple instances of these interactive blocks can be added to the page, and they will all share and update the same global counter value.
Local context
Local context in the Interactivity API refers to local data defined within a specific element in the HTML structure. Unlike global state, local context is only accessible to the element where it’s defined and its child elements.
The local context is particularly useful when you need independent state for individual interactive blocks, ensuring that each instance of a block can maintain its own unique data without interfering with others.
You should use local context when:
- You need to maintain separate state for multiple instances of the same interactive block.
- You want to encapsulate data that’s only relevant to a specific interactive block and its children.
- You need to implement features that are isolated to a specific part of your UI.
Working with local context
- Initializing the local context
The local context is initialized directly within the HTML structure using the
data-wp-context
directive. This directive accepts a JSON string that defines the initial values for that piece of context.<div data-wp-context='{ "counter": 0 }'> <!-- Child elements will have access to `context.counter` --> </div>
You can also initialize the local context on the server using the
wp_interactivity_data_wp_context
PHP helper, which ensures proper escaping and formatting of the stringified values:<?php $context = array( 'counter' => 0 ); ?> <div <?php echo wp_interactivity_data_wp_context( $context ); ?>> <!-- Child elements will have access to `context.counter` --> </div>
- Accessing the local context
In the HTML markup, you can access the local context values directly by referencing
context
in the directive values:<div data-wp-bind--hidden="!context.isOpen"> <span data-wp-text="context.counter"></span> </div>
In JavaScript, you can access the local context values using the
getContext
function:store( 'myPlugin', { actions: { sendAnalyticsEvent() { const { counter } = getContext(); myAnalyticsLibrary.sendEvent( 'updated counter', counter ); }, }, callbacks: { logCounter() { const { counter } = getContext(); console.log( `Current counter: ${ counter }` ); }, }, } );
The
getContext
function returns the local context of the element that triggered the action/callback execution. -
Updating the local context
To update the local context values in JavaScript, you can modify the object returned by
getContext
:store( 'myPlugin', { actions: { increment() { const context = getContext(); context.counter += 1; }, updateName( event ) { const context = getContext(); context.name = event.target.value; }, }, } );
Changes to the local context will automatically trigger updates in any directives that depend on the modified values.
Please, visit The Reactive and Declarative mindset guide to learn more about how reactivity works in the Interactivity API.
-
Nesting local contexts
Local contexts can be nested, with child contexts inheriting and potentially overriding values from parent contexts:
<div data-wp-context='{ "theme": "light", "counter": 0 }'> <p>Theme: <span data-wp-text="context.theme"></span></p> <p>Counter: <span data-wp-text="context.counter"></span></p> <div data-wp-context='{ "theme": "dark" }'> <p>Theme: <span data-wp-text="context.theme"></span></p> <p>Counter: <span data-wp-text="context.counter"></span></p> </div> </div>
In this example, the inner
div
will have atheme
value of"dark"
, but will inherit thecounter
value0
from its parent context.
Example: One interactive block using local context to have independent state
In this example, there is a single interactive block that shows a counter and can increment it. By using local context, each instance of this block will have its own independent counter, even if multiple blocks are added to the page.
<div
data-wp-interactive="myCounterPlugin"
<?php echo get_block_wrapper_attributes(); ?>
data-wp-context='{ "counter": 0 }'
>
<p>Counter: <span data-wp-text="context.counter"></span></p>
<button data-wp-on-async--click="actions.increment">Increment</button>
</div>
store( 'myCounterPlugin', {
actions: {
increment() {
const context = getContext();
context.counter += 1;
},
},
} );
In this example:
- A local context with an initial
counter
value of0
is defined using thedata-wp-context
directive. - The counter is displayed using
data-wp-text="context.counter"
, which reads from the local context. - The increment button uses
data-wp-on-async--click="actions.increment"
to trigger the increment action. - In JavaScript, the
getContext
function is used to access and modify the local context for each block instance.
A user will be able to add multiple instances of this block to a page, and each will maintain its own independent counter. Clicking the “Increment” button on one block will only affect that specific block’s counter and not the others.
Derived state
Derived state in the Interactivity API refers to a value that is computed from other parts of the global state or local context. It’s calculated on demand rather than stored. It ensures consistency, reduces redundancies, and enhances the declarative nature of your code.
Derived state is a fundamental concept in modern state management, not unique to the Interactivity API. It’s also used in other popular state management systems like Redux, where it’s called selectors
, or Preact Signals, where it’s known as computed
values.
Derived state offers several key benefits that make it an essential part of a well-designed application state, including:
- Single source of truth: Derived state encourages you to store only the essential, raw data in your state. Any values that can be calculated from this core data become derived state. This approach reduces the risk of inconsistencies in your interactive blocks.
-
Automatic updates: When you use derived state, values are recalculated automatically whenever the underlying data changes. This ensures that all parts of your interactive blocks always have access to the most up-to-date information without manual intervention.
-
Simplified state management: By computing values on-demand rather than storing and updating them manually, you reduce the complexity of your state management logic. This leads to cleaner, more maintainable code.
-
Improved performance: In many cases, derived state can be optimized to recalculate only when necessary, potentially improving your interactive blocks’ performance.
-
Easier debugging: With derived state, it’s clearer where data originates and how it’s transformed. This can make it easier to track down issues in your interactive blocks.
In essence, derived state allows you to express relationships between different pieces of data in your interactive blocks declaratively, instead of imperatively updating related values whenever something changes.
Please, visit The Reactive and Declarative mindset guide to learn more about how to leverage declarative coding in the Interactivity API.
You should use derived state:
- When a part of your global state or local context can be computed from other state values.
- To avoid redundant data that needs to be manually kept in sync.
- To ensure consistency across your interactive blocks by automatically updating derived values.
- To simplify your actions by removing the need to update multiple related state properties.
Working with derived state
- Initializing the derived state
Typically, the derived state should be initialized on the server using the
wp_interactivity_state
function in the exact same way as the global state.- When the initial value is known and static, it can be defined directly:
wp_interactivity_state( 'myCounterPlugin', array( 'counter' => 1, // This is global state. 'double' => 2, // This is derived state. ));
- Or it can be defined by doing the necessary computations:
$counter = 1; $double = $counter * 2; wp_interactivity_state( 'myCounterPlugin', array( 'counter' => $counter, // This is global state. 'double' => $double, // This is derived state. ));
Regardless of the approach, the initial derived state values will be used during the rendering of the page in PHP, and the HTML can be populated with the correct values.
Please, visit the Server-side Rendering guide to learn more about how directives are processed on the server.
The same mechanism applies even when the derived state property depends on the local context.
<?php $counter = 1; // This is the local context. $context = array( 'counter' => $counter ); wp_interactivity_state( 'myCounterPlugin', array( 'double' => $counter * 2, // This is derived state. )); ?> <div data-wp-interactive="myCounterPlugin" <?php echo wp_interactivity_data_wp_context( $context ); ?> > <div> Counter: <span data-wp-text="context.counter"></span> </div> <div> Double: <span data-wp-text="state.double"></span> </div> </div>
In JavaScript, the derived state is defined using getters:
const { state } = store( 'myCounterPlugin', { state: { get double() { return state.counter * 2; }, }, } );
Derived state can depend on local context, or local context and global state at the same time.
const { state } = store( 'myCounterPlugin', { state: { get double() { const { counter } = getContext(); // Depends on local context. return counter * 2; }, get product() { const { counter } = getContext(); // Depends on local context and global state. return counter * state.factor; }, }, } );
In some cases, when the derived state depends on the local context and the local context can change dynamically in the server, instead of the initial derived state, you can use a function (Closure) that calculates it dynamically.
<?php wp_interactivity_state( 'myProductPlugin', array( 'list' => array( 1, 2, 3 ), 'factor' => 3, 'product' => function() { $state = wp_interactivity_state(); $context = wp_interactivity_get_context(); return $context['item'] * $state['factor']; } )); ?> <template data-wp-interactive="myProductPlugin" data-wp-each="state.list" > <span data-wp-text="state.product"></span> </template>
This
data-wp-each
template will render this HTML (directives omitted):<span>3</span> <span>6</span> <span>9</span>
- When the initial value is known and static, it can be defined directly:
- Accessing the derived state
In the HTML markup, the syntax for the derived state is the same as the one for the global state, just by referencing
state
in the directive attribute values.<span data-wp-text="state.double"></span>
The same happens in JavaScript. Both global state and derived state can be consumed through the
state
property of the store:const { state } = store( 'myCounterPlugin', { // ... actions: { readValues() { state.counter; // Regular state, returns 1. state.double; // Derived state, returns 2. }, }, } );
This lack of distinction is intentional, allowing developers to consume both derived and global state uniformly, and making them interchangeable in practice.
You can also access the derived state from another derived state and, thus, create multiple levels of computed values.
const { state } = store( 'myPlugin', { state: { get double() { return state.counter * 2; }, get doublePlusOne() { return state.double + 1; }, }, } );
- Updating the derived state
The derived state cannot be updated directly. To update its values, you need to update the global state or local context on which that derived state depends.
const { state } = store( 'myCounterPlugin', { // ... actions: { updateValues() { state.counter; // Regular state, returns 1. state.double; // Derived state, returns 2. state.counter = 2; state.counter; // Regular state, returns 2. state.double; // Derived state, returns 4. }, }, } );
Example: Not using derived state vs using derived state
Let’s consider a scenario where there is a counter and the double value needs to be displayed, and let’s compare two approaches: one without derived state and one with derived state.
- Not using derived state
const { state } = store( 'myCounterPlugin', { state: { counter: 1, double: 2, }, actions: { increment() { state.counter += 1; state.double = state.counter * 2; }, }, } );
In this approach, both the
state.counter
andstate.double
values are manually updated in theincrement
action. While this works, it has several drawbacks:- It’s less declarative.
- It can lead to bugs if
state.counter
is updated from multiple places and developers forget to keepstate.double
in sync. - It requires more cognitive load to remember to update related values.
- Using derived state
const { state } = store( 'myCounterPlugin', { state: { counter: 1, get double() { return state.counter * 2; }, }, actions: { increment() { state.counter += 1; }, }, } );
In this improved version:
state.double
is defined as a getter, automatically deriving its value fromstate.counter
.- The
increment
action only needs to updatestate.counter
. state.double
is always guaranteed to have the correct value, regardless of how or wherestate.counter
is updated.
Example: Using derived state with local context
Let’s now consider a scenario where there is a local context that initializes a counter.
store( 'myCounterPlugin', {
state: {
get double() {
const { counter } = getContext();
return counter * 2;
},
},
actions: {
increment() {
const context = getContext();
context.counter += 1;
},
},
} );
<div data-wp-interactive="myCounterPlugin">
<!-- This will render "Double: 2" -->
<div data-wp-context='{ "counter": 1 }'>
Double: <span data-wp-text="state.double"></span>
<!-- This button will increment the local counter. -->
<button data-wp-on-async--click="actions.increment">Increment</button>
</div>
<!-- This will render "Double: 4" -->
<div data-wp-context='{ "counter": 2 }'>
Double: <span data-wp-text="state.double"></span>
<!-- This button will increment the local counter. -->
<button data-wp-on-async--click="actions.increment">Increment</button>
</div>
</div>
In this example, the derived state state.double
reads from the local context present in each element and returns the correct value for each instance where it is used.
Example: Using derived state with both local context and global state
Let’s now consider a scenario where there are a global tax rate and local product prices and calculate the final price, including tax.
<div
data-wp-interactive="myProductPlugin"
data-wp-context='{ "priceWithoutTax": 100 }'
>
<p>Product Price: $<span data-wp-text="context.priceWithoutTax"></span></p>
<p>Tax Rate: <span data-wp-text="state.taxRatePercentage"></span></p>
<p>Price (inc. tax): $<span data-wp-text="state.priceWithTax"></span></p>
</div>
const { state } = store( 'myProductPlugin', {
state: {
taxRate: 0.21,
get taxRatePercentage() {
return `${ state.taxRate * 100 }%`;
},
get priceWithTax() {
const { priceWithoutTax } = getContext();
return price * ( 1 + state.taxRate );
},
},
actions: {
updateTaxRate( event ) {
// Updates the global tax rate.
state.taxRate = event.target.value;
},
updatePrice( event ) {
// Updates the local product price.
const context = getContext();
context.priceWithoutTax = event.target.value;
},
},
} );
In this example, priceWithTax
is derived from both the global taxRate
and the local priceWithoutTax
. Every time you update the global state or local context through the updateTaxRate
or updatePrice
actions, the Interactivity API recomputes the derived state and updates the necessary parts of the DOM.
By using derived state, you create a more maintainable and less error-prone codebase. It ensures that related state values are always in sync, reduces the complexity of your actions, and makes your code more declarative and easier to reason about.
Conclusion
Remember, the key to effective state management is to keep your state minimal and avoid redundancy. Use derived state to compute values dynamically, and choose between global state and local context based on the scope and requirements of your data. This will lead to a cleaner, more robust architecture that is easier to debug and maintain.