Skip to content

Commit c309f28

Browse files
committed
Interactivity API: Support lazy-loaded derived state props.
Serialize on the server the paths of derived state closures accessed during directive processing and include them in the serialized data. During the Interactivity API hydration, if a directive reads one of those paths before its JavaScript getter is available, keep the server-rendered value intact instead of replacing it with `undefined`. props darerodz. Fixes #63898. git-svn-id: https://develop.svn.wordpress.org/trunk@60953 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 83b0df6 commit c309f28

File tree

3 files changed

+218
-64
lines changed

3 files changed

+218
-64
lines changed

src/wp-includes/interactivity-api/class-wp-interactivity-api.php

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ final class WP_Interactivity_API {
5858
*/
5959
private $config_data = array();
6060

61+
/**
62+
* Keeps track of all derived state closures accessed during server-side rendering.
63+
*
64+
* This data is serialized and sent to the client as part of the interactivity
65+
* data, and is handled later in the client to support derived state props that
66+
* are lazily hydrated.
67+
*
68+
* @since 6.9.0
69+
* @var array
70+
*/
71+
private $derived_state_closures = array();
72+
6173
/**
6274
* Flag that indicates whether the `data-wp-router-region` directive has
6375
* been found in the HTML and processed.
@@ -236,12 +248,17 @@ public function filter_script_module_interactivity_router_data( array $data ): a
236248
* interactivity stores and the configuration will be available using a `getConfig` utility.
237249
*
238250
* @since 6.7.0
251+
* @since 6.9.0 Serializes derived state props accessed during directive processing.
239252
*
240253
* @param array $data Data to filter.
241254
* @return array Data for the Interactivity API script module.
242255
*/
243256
public function filter_script_module_interactivity_data( array $data ): array {
244-
if ( empty( $this->state_data ) && empty( $this->config_data ) ) {
257+
if (
258+
empty( $this->state_data ) &&
259+
empty( $this->config_data ) &&
260+
empty( $this->derived_state_closures )
261+
) {
245262
return $data;
246263
}
247264

@@ -265,6 +282,16 @@ public function filter_script_module_interactivity_data( array $data ): array {
265282
$data['state'] = $state;
266283
}
267284

285+
$derived_props = array();
286+
foreach ( $this->derived_state_closures as $key => $value ) {
287+
if ( ! empty( $value ) ) {
288+
$derived_props[ $key ] = $value;
289+
}
290+
}
291+
if ( ! empty( $derived_props ) ) {
292+
$data['derivedStateClosures'] = $derived_props;
293+
}
294+
268295
return $data;
269296
}
270297

@@ -598,7 +625,7 @@ private function evaluate( $directive_value ) {
598625
// Extracts the value from the store using the reference path.
599626
$path_segments = explode( '.', $path );
600627
$current = $store;
601-
foreach ( $path_segments as $path_segment ) {
628+
foreach ( $path_segments as $index => $path_segment ) {
602629
/*
603630
* Special case for numeric arrays and strings. Add length
604631
* property mimicking JavaScript behavior.
@@ -647,6 +674,20 @@ private function evaluate( $directive_value ) {
647674
array_push( $this->namespace_stack, $ns );
648675
try {
649676
$current = $current();
677+
678+
/*
679+
* Tracks derived state properties that are accessed during
680+
* rendering.
681+
*
682+
* @since 6.9.0
683+
*/
684+
$this->derived_state_closures[ $ns ] = $this->derived_state_closures[ $ns ] ?? array();
685+
686+
// Builds path for the current property and add it to tracking if not already present.
687+
$current_path = implode( '.', array_slice( $path_segments, 0, $index + 1 ) );
688+
if ( ! in_array( $current_path, $this->derived_state_closures[ $ns ], true ) ) {
689+
$this->derived_state_closures[ $ns ][] = $current_path;
690+
}
650691
} catch ( Throwable $e ) {
651692
_doing_it_wrong(
652693
__METHOD__,
@@ -1160,6 +1201,7 @@ private function data_wp_router_region_processor( WP_Interactivity_API_Directive
11601201
* `template` tag.
11611202
*
11621203
* @since 6.5.0
1204+
* @since 6.9.0 Include the list path in the rendered `data-wp-each-child` directives.
11631205
*
11641206
* @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
11651207
* @param string $mode Whether the processing is entering or exiting the tag.
@@ -1205,8 +1247,8 @@ private function data_wp_each_processor( WP_Interactivity_API_Directives_Process
12051247
}
12061248

12071249
// Extracts the namespace from the directive attribute value.
1208-
$namespace_value = end( $this->namespace_stack );
1209-
list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value )
1250+
$namespace_value = end( $this->namespace_stack );
1251+
list( $namespace_value, $path ) = is_string( $attribute_value ) && ! empty( $attribute_value )
12101252
? $this->extract_directive_value( $attribute_value, $namespace_value )
12111253
: array( $namespace_value, null );
12121254

@@ -1228,10 +1270,20 @@ private function data_wp_each_processor( WP_Interactivity_API_Directives_Process
12281270
return;
12291271
}
12301272

1231-
// Adds the `data-wp-each-child` to each top-level tag.
1273+
/*
1274+
* Adds the `data-wp-each-child` directive to each top-level tag
1275+
* rendered by this `data-wp-each` directive. The value is the
1276+
* `data-wp-each` directive's namespace and path.
1277+
*
1278+
* Nested `data-wp-each` directives could render
1279+
* `data-wp-each-child` elements at the top level as well, and
1280+
* they should be overwritten.
1281+
*
1282+
* @since 6.9.0
1283+
*/
12321284
$i = new WP_Interactivity_API_Directives_Processor( $processed_item );
12331285
while ( $i->next_tag() ) {
1234-
$i->set_attribute( 'data-wp-each-child', true );
1286+
$i->set_attribute( 'data-wp-each-child', $namespace_value . '::' . $path );
12351287
$i->next_balanced_tag_closer_tag();
12361288
}
12371289
$processed_content .= $i->get_updated_html();

0 commit comments

Comments
 (0)