Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactivity API: Correctly handle lazily added, deeply nested properties in context #65465

Draft
wants to merge 3 commits into
base: trunk
Choose a base branch
from

Conversation

michalczaplinski
Copy link
Contributor

@michalczaplinski michalczaplinski commented Sep 18, 2024

Description

This PR addresses an issue in the Interactivity API where deeply nested properties that are initially undefined and later added to the context were not being properly handled. The change ensures that effects dependent on these properties are correctly triggered when the properties are added, even if they initially undefined.

Changes

  • Added a new test case in packages/interactivity/src/proxies/test/context-proxy.ts to demonstrate the desired behavior for deeply nested properties.
  • (TODO) Implement the necessary changes in the proxifyContext or deepMerge function to support this behavior.

Testing

A new test case has been added to verify the correct handling of deeply nested properties

TODO

  • Implement the necessary changes in the proxifyContext() or deepMerge() function to make the new test pass.

Add a test to ensure the context proxy correctly handles and updates
deeply nested properties that are initially undefined.
@michalczaplinski michalczaplinski added [Type] Bug An existing feature does not function as intended [Feature] Interactivity API API to add frontend interactivity to blocks. [Packages] Interactivity /packages/interactivity labels Sep 18, 2024
Comment on lines +297 to +299
// The effect should be called again
expect( spy ).toHaveBeenCalledTimes( 2 );
expect( deepValue ).toBe( 'test value' );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DAreRodz both of those assertions fail:

  • expect( spy ).toHaveBeenCalledTimes( 2 );

     Expected number of calls: 2
     Received number of calls: 1
    
  • expect( deepValue ).toBe( 'test value' );

     Expected: "test value"
     Received: undefined
    

But I'm not 100% sure if those are precisely the failures we wanted to see.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, @michalczaplinski. 😁

I'm not 100% sure if those are precisely the failures we wanted to see.

They are. The values received are the same as those returned in the initial call. Basically, the spy function has not been executed again because the signal for context.a has not been updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to stare at the test a little longer to convince myself that it was actually the case 😅.

Excellent, I'll add the fix tomorrow 🙂

@michalczaplinski michalczaplinski added the Backport to WP 6.7 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Sep 19, 2024
@michalczaplinski
Copy link
Contributor Author

@DAreRodz

As we discussed, I was expecting the unit test to fail, but after updating the test cases they are actually passing!


Here's what's going on. Let's assume I'm going through an example that follows the newly added unit tests in this PR - meaning I'm trying to follow what's going on when running the following code:

const fallback: any = proxifyContext(
	proxifyState( 'test', {} ),
	{}
);

const context: any = proxifyContext(
	proxifyState( 'test', {} ),
	fallback
);

deepMerge( context, { a: { b: { c: { d: 'test value' } } } } );

When we set a property on the context using deepMerge(), it enters into this code path and tries to set the value:

if ( isNew ) {
target[ key ] = {};
}

This triggers the set() trap of the state proxy which calls Reflect.set() here:

return Reflect.set( target, key, value, receiver );

At this point:

  • target is a Proxied object ({})
  • key is a
  • value is {}

Since the key does not exist on target, this further triggers the defineProperty() trap. It is there that the value is finally set:

const result = Reflect.defineProperty( target, key, desc );

However, the defineProperty() trap also creates a signal and proxifies the state!

prop.setValue(
shouldProxy( value ) ? proxifyState( ns, value ) : value
);

After that, context.a is proxied. When the defineProperty() trap and subsequently the set() trap returns, we are back here:

deepMergeRecursive( target[ key ], source[ key ], override );
} else if ( override || isNew ) {

As we call deepMergeRecursive, we'll recurse one level lower and repeat the process until the whole nested object is proxified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Backport to WP 6.7 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta [Feature] Interactivity API API to add frontend interactivity to blocks. [Packages] Interactivity /packages/interactivity [Type] Bug An existing feature does not function as intended
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants