+```html
+
```
-These values assigned to directives are **references** to a particular property in the store. They are wired to the directives automatically so that each directive “knows” what store element (action, effect...) refers to without any additional configuration.
+These values assigned to directives are **references** to a particular property in the store. They are wired to the directives automatically so that each directive “knows” what store element refers to, without any additional configuration.
+Note that, by default, references point to properties in the current namespace, which is the one specified by the closest ancestor with a `data-wp-interactive` attribute. If you need to access a property from a different namespace, you can explicitly set the namespace where the property we want to access is defined. The syntax is `namespace::reference`, replacing `namespace` with the appropriate value.
-## The store
+In the example below, we get `state.isPlaying` from `otherPlugin` instead of `myPlugin`:
-The store is used to create the logic (actions, effects…) linked to the directives and the data used inside that logic (state, selectors…).
+```html
+
+```
-**The store is usually created in the `view.js` file of each block**, although it can be initialized from the `render.php` of the block.
+## The store
-The store contains the reactive state and the actions and effects that modify it.
+The store is used to create the logic (actions, side effects…) linked to the directives and the data used inside that logic (state, derived state…).
+
+**The store is usually created in the `view.js` file of each block**, although the state can be initialized from the `render.php` of the block.
### Elements of the store
#### State
-Defines data available to the HTML nodes of the page. It is important to differentiate between two ways to define the data:
- - **Global state**: It is defined using the `store()` function, and the data is available to all the HTML nodes of the page. It can be accessed using the `state` property.
- - **Context/Local State**: It is defined using the `data-wp-context` directive in an HTML node, and the data is available to that HTML node and its children. It can be accessed using the `context` property.
-
+It defines data available to the HTML nodes of the page. It is important to differentiate between two ways to define the data:
+
+- **Global state**: It is defined using the `store()` function with the `state` property, and the data is available to all the HTML nodes of the page.
+- **Context/Local State**: It is defined using the `data-wp-context` directive in an HTML node, and the data is available to that HTML node and its children. It can be accessed using the `getContext` function inside of an action, derived state or side effect.
+
```html
@@ -531,13 +562,15 @@ Defines data available to the HTML nodes of the page. It is important to differe
```
```js
-store( {
+const { state } = store( "myPlugin", {
state: {
someText: "Hello Universe!"
},
actions: {
- someAction: ({ state, context }) => {
+ someAction: () => {
state.someText // Access or modify global state - "Hello Universe!"
+
+ const context = getContext();
context.someText // Access or modify local state (context) - "Hello World!"
},
},
@@ -548,96 +581,126 @@ store( {
Usually triggered by the `data-wp-on` directive (using event listeners) or other actions.
-#### Effects
+#### Side Effects
-Automatically react to state changes. Usually triggered by `data-wp-effect` or `data-wp-init` directives.
+Automatically react to state changes. Usually triggered by `data-wp-watch` or `data-wp-init` directives.
-#### Selectors
+#### Derived state
-Also known as _derived state_, returns a computed version of the state. They can access both `state` and `context`.
+They return a computed version of the state. They can access both `state` and `context`.
```js
// view.js
-store( {
- state: {
- amount: 34,
- defaultCurrency: 'EUR',
- currencyExchange: {
- USD: 1.1,
- GBP: 0.85,
- },
- },
- selectors: {
- amountInUSD: ( { state } ) =>
- state.currencyExchange[ 'USD' ] * state.amount,
- amountInGBP: ( { state } ) =>
- state.currencyExchange[ 'GBP' ] * state.amount,
- },
+const { state } = store( "myPlugin", {
+ state: {
+ amount: 34,
+ defaultCurrency: 'EUR',
+ currencyExchange: {
+ USD: 1.1,
+ GBP: 0.85,
+ },
+ get amountInUSD() {
+ return state.currencyExchange[ 'USD' ] * state.amount,
+ },
+ get amountInGBP() {
+ return state.currencyExchange[ 'GBP' ] * state.amount,
+ },
+ },
+} );
+```
+
+### Accessing data in callbacks
+
+
+The **`store`** contains all the store properties, like `state`, `actions` or `callbacks`. They are returned by the `store()` call, so you can access them by destructuring it:
+
+```js
+const { state, actions } = store( "myPlugin", {
+ // ...
} );
```
-### Arguments passed to callbacks
+The `store()` function can be called multiple times and all the store parts will be merged together:
+
+```js
+store( "myPlugin", {
+ state: {
+ someValue: 1,
+ }
+} );
+
+const { state } = store( "myPlugin", {
+ actions: {
+ someAction() {
+ state.someValue // = 1
+ }
+ }
+} );
+```
-When a directive is evaluated, the reference callback receives an object with:
+> **Note**
+> All `store()` calls with the same namespace return the same references, i.e., the same `state`, `actions`, etc., containing the result of merging all the store parts passed.
-- The **`store`** containing all the store properties, like `state`, `selectors`, `actions` or `effects`
-- The **context** (an object containing the context defined in all the `wp-context` ancestors).
-- The reference to the DOM element on which the directive was defined (a `ref`).
-- Other properties relevant to the directive. For example, the `data-wp-on--click` directive also receives the instance of the [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) triggered by the user.
+- To access the context inside an action, derived state, or side effect, you can use the `getContext` function.
+- To access the reference, you can use the `getElement` function.
-_Example of action making use of all values received when it's triggered_
```js
-// view.js
-store( {
- state: {
- theme: false,
- },
- actions: {
- toggle: ( { state, context, ref, event, className } ) => {
- console.log( state );
- // `{ "theme": false }`
- console.log( context );
- // `{ "isOpen": true }`
- console.log( ref );
- // The DOM element
- console.log( event );
- // The Event object if using the `data-wp-on`
- console.log( className );
- // The class name if using the `data-wp-class`
- },
- },
+const { state } = store( "myPlugin", {
+ state: {
+ get someDerivedValue() {
+ const context = getContext();
+ const { ref } = getElement();
+ // ...
+ }
+ },
+ actions: {
+ someAction() {
+ const context = getContext();
+ const { ref } = getElement();
+ // ...
+ }
+ },
+ callbacks: {
+ someEffect() {
+ const context = getContext();
+ const { ref } = getElement();
+ // ...
+ }
+ }
} );
```
This approach enables some functionalities that make directives flexible and powerful:
-- Actions and effects can read and modify the state and the context.
+- Actions and side effects can read and modify the state and the context.
- Actions and state in blocks can be accessed by other blocks.
-- Actions and effects can do anything a regular JavaScript function can do, like access the DOM or make API requests.
-- Effects automatically react to state changes.
+- Actions and side effects can do anything a regular JavaScript function can do, like access the DOM or make API requests.
+- Side effects automatically react to state changes.
### Setting the store
#### On the client side
-*In the `view.js` file of each block* we can define both the state and the elements of the store referencing functions like actions, effects or selectors.
+*In the `view.js` file of each block* we can define both the state and the elements of the store referencing functions like actions, side effects or derived state.
-`store` method used to set the store in javascript can be imported from `@wordpress/interactivity`.
+The `store` method used to set the store in javascript can be imported from `@wordpress/interactivity`.
```js
// store
-import { store } from '@wordpress/interactivity';
+import { store, getContext } from '@wordpress/interactivity';
-store( {
+store( "myPlugin", {
actions: {
- toggle: ( { context } ) => {
+ toggle: () => {
+ const context = getContext();
context.isOpen = !context.isOpen;
},
},
- effects: {
- logIsOpen: ( { context } ) => {
+ callbacks: {
+ logIsOpen: () => {
+ const { isOpen } = getContext();
// Log the value of `isOpen` each time it changes.
- console.log( `Is open: ${ context.isOpen }` );
+ console.log( `Is open: ${ isOpen }` );
}
},
});
@@ -645,69 +708,66 @@ store( {
#### On the server side
-The store can also be initialized on the server using the `wp_store()` function. You would typically do this in the `render.php` file of your block (the `render.php` templates were [introduced](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/) in WordPress 6.1).
+> **Note**
+> We will rename `wp_store` to `wp_initial_state` in a future version.
+
+The state can also be initialized on the server using the `wp_store()` function. You would typically do this in the `render.php` file of your block (the `render.php` templates were [introduced](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/) in WordPress 6.1).
-The store defined on the server with `wp_store()` gets merged with the stores defined in the view.js files.
+The state defined on the server with `wp_store()` gets merged with the stores defined in the view.js files.
The `wp_store` function receives an [associative array](https://www.php.net/manual/en/language.types.array.php) as a parameter.
-
_Example of store initialized from the server with a `state` = `{ someValue: 123 }`_
```php
// render.php
wp_store( array(
- 'state' => array(
- 'myNamespace' => array(
- 'someValue' = 123
- )
+ 'myPlugin' => array(
+ 'someValue' = 123
)
);
```
-Initializing the store in the server also allows you to use any WordPress API. For example, you could use the Core Translation API to translate part of your state:
+Initializing the state in the server also allows you to use any WordPress API. For example, you could use the Core Translation API to translate part of your state:
```php
// render.php
wp_store(
array(
- "state" => array(
- "favoriteMovies" => array(
- "1" => array(
- "id" => "123-abc",
- "movieName" => __("someMovieName", "textdomain")
- ),
+ "favoriteMovies" => array(
+ "1" => array(
+ "id" => "123-abc",
+ "movieName" => __("someMovieName", "textdomain")
),
),
)
);
```
-### Store options
-
-The `store` function accepts an object as a second argument with the following optional properties:
+### Private stores
-#### `afterLoad`
-
-Callback to be executed after the Interactivity API has been set up and the store is ready. It receives the global store as argument.
+A given store namespace can be marked as private, thus preventing its content to be accessed from other namespaces. The mechanism to do so is by adding a `lock` option to the `store()` call, like shown in the example below. This way, further executions of `store()` with the same locked namespace will throw an error, meaning that the namespace can only be accessed where its reference was returned from the first `store()` call. This is specially useful for developers that want to hide part of their plugin stores so it doesn't become accessible for extenders.
```js
-// view.js
-store(
- {
- state: {
- cart: [],
- },
- },
- {
- afterLoad: async ( { state } ) => {
- // Let's consider `clientId` is added
- // during server-side rendering.
- state.cart = await getCartData( state.clientId );
- },
- }
+const { state } = store(
+ "myPlugin/private",
+ { state: { messages: [ "private message" ] } },
+ { lock: true }
);
+
+// The following call throws an Error!
+store( "myPlugin/private", { /* store part */ } );
```
+There is also a way to unlock private stores: instead of passing a boolean, you can use a string as the `lock` value. Such a string can then be used in subsequent `store()` calls to the same namespace to unlock its content. Only the code knowing the string lock will be able to unlock the protected store namespaced. This is useful for complex stores defined in multiple JS modules.
+```js
+const { state } = store(
+ "myPlugin/private",
+ { state: { messages: [ "private message" ] } },
+ { lock: PRIVATE_LOCK }
+);
+// The following call works as expected.
+store( "myPlugin/private", { /* store part */ }, { lock: PRIVATE_LOCK } );
+```
diff --git a/packages/interactivity/docs/assets/state-directives.png b/packages/interactivity/docs/assets/state-directives.png
index feb93a2d1f8956..a2422d1a2a049e 100644
Binary files a/packages/interactivity/docs/assets/state-directives.png and b/packages/interactivity/docs/assets/state-directives.png differ
diff --git a/packages/interactivity/docs/assets/store-server-client.png b/packages/interactivity/docs/assets/store-server-client.png
index 089268cdc7d9c7..37818e37faa3dc 100644
Binary files a/packages/interactivity/docs/assets/store-server-client.png and b/packages/interactivity/docs/assets/store-server-client.png differ