@@ -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