Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
*/

if ( $attributes['disableNavigation'] ) {
if ( isset( $attributes['disableNavigation'] ) && $attributes['disableNavigation'] ) {
wp_interactivity_config(
'core/router',
array( 'clientNavigationDisabled' => true )
Expand All @@ -20,6 +20,31 @@
array( 'data' => $attributes['data'] )
);
}

if ( isset( $attributes['derivedStateClosure'] ) && $attributes['derivedStateClosure'] ) {
wp_interactivity_state(
'router/derived-state',
array(
'derivedStateClosure' => function () {
$context = wp_interactivity_get_context();
return $context['value'] . 'FromClosure';
},
)
);

add_filter(
'script_module_data_@wordpress/interactivity',
function ( $data ) {
if ( ! isset( $data ) ) {
$data = array();
}
$data['derivedStateClosures'] = array(
'router/derived-state' => array( 'state.derivedStateClosure' ),
);
return $data;
}
);
}
?>

<div
Expand Down Expand Up @@ -75,3 +100,7 @@
<div data-testid="prop2" data-wp-text="state.data.prop2"></div>
<div data-testid="prop3" data-wp-text="state.data.prop3"></div>
</div>

<div data-wp-interactive="router/derived-state" data-wp-context='{"value": "hello"}'>
<div data-testid="derivedStateClosure" data-wp-text="state.derivedStateClosure">helloFromClosure</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { store, withSyncEvent } from '@wordpress/interactivity';
import { store, withSyncEvent, getContext } from '@wordpress/interactivity';

const { state } = store( 'router', {
state: {
Expand Down Expand Up @@ -31,11 +31,16 @@ const { state } = store( 'router', {
const { actions } = yield import(
'@wordpress/interactivity-router'
);
yield actions.navigate( e.target.href, { force, timeout } );

try {
yield actions.navigate( e.target.href, { force, timeout } );
} catch ( error ) {
state.status = 'fail';
}

state.navigations.pending -= 1;

if ( state.navigations.pending === 0 ) {
if ( state.navigations.pending === 0 && state.status === 'busy' ) {
state.status = 'idle';
}
} ),
Expand All @@ -44,3 +49,12 @@ const { state } = store( 'router', {
},
},
} );

store( 'router/derived-state', {
state: {
get derivedStateClosure() {
const { value } = getContext();
return `${ value }FromGetter`;
},
},
} );
4 changes: 4 additions & 0 deletions packages/interactivity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fixes

- Fix derived state closures processing on client-side navigation. ([#72725](https://github.com/WordPress/gutenberg/pull/72725))

## 6.33.0 (2025-10-17)

### Enhancements
Expand Down
16 changes: 13 additions & 3 deletions packages/interactivity/src/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { proxifyState, proxifyStore, deepMerge } from './proxies';
import { proxifyState, proxifyStore, deepMerge, peek } from './proxies';
import { PENDING_GETTER } from './proxies/state';
import { getNamespace } from './namespaces';
import { isPlainObject, deepReadOnly, navigationSignal } from './utils';
Expand Down Expand Up @@ -315,10 +315,20 @@ export const populateServerData = ( data?: {
const pathParts = path.split( '.' );
const prop = pathParts.splice( -1, 1 )[ 0 ];
const parent = pathParts.reduce(
( prev, key ) => prev[ key ],
( prev, key ) => peek( prev, key ),
st
);
if ( isPlainObject( parent[ prop ] ) ) {

// Get the descriptor of the derived state prop.
const desc = Object.getOwnPropertyDescriptor(
parent,
prop
);

// The derived state prop is considered a pending getter
// only if its value is a plain object, which is how
// closures are serialized from PHP.
if ( isPlainObject( desc?.value ) ) {
parent[ prop ] = PENDING_GETTER;
}
} );
Expand Down
42 changes: 41 additions & 1 deletion test/e2e/specs/interactivity/router-navigate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,19 @@ test.describe( 'Router navigate', () => {
},
},
} );

const link4 = await utils.addPostWithBlock( 'test/router-navigate', {
alias: 'router navigate - closure',
attributes: {
title: 'Link with derivedStateClosure',
derivedStateClosure: true,
},
} );
await utils.addPostWithBlock( 'test/router-navigate', {
alias: 'router navigate - main',
attributes: {
title: 'Main',
links: [ link1, link2, link3 ],
links: [ link1, link2, link3, link4 ],
data: {
getterProp: 'value from main',
prop1: 'main',
Expand Down Expand Up @@ -285,4 +293,36 @@ test.describe( 'Router navigate', () => {
await expect( title ).toHaveText( 'Main (navigation disabled)' );
await expect( count ).toHaveText( '0' );
} );

test( 'should support derived state closures', async ( { page } ) => {
const count = page.getByTestId( 'router navigations count' );
const status = page.getByTestId( 'router status' );
const title = page.getByTestId( 'title' );
const derivedStateClosure = page.getByTestId( 'derivedStateClosure' );

// Check the count to ensure the page has hydrated.
await expect( count ).toHaveText( '0' );

// Ensure the value from the getter is correct.
await expect( derivedStateClosure ).toHaveText( 'helloFromGetter' );

// Navigate to a page without clientNavigationDisabled.
await page.getByTestId( 'link 4' ).click();

// Check the page has updated and the navigation was successfull.
await expect( status ).toHaveText( 'idle' );
await expect( title ).toHaveText( 'Link with derivedStateClosure' );
await expect( count ).toHaveText( '1' );

// Ensure the value from the getter has not changed.
await expect( derivedStateClosure ).toHaveText( 'helloFromGetter' );

await page.goBack();
await expect( status ).toHaveText( 'idle' );
await expect( title ).toHaveText( 'Main' );
await expect( count ).toHaveText( '1' );

// Ensure the value from the getter has not changed.
await expect( derivedStateClosure ).toHaveText( 'helloFromGetter' );
} );
} );
Loading