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 with deepMerge() #65465

Merged
merged 11 commits into from
Oct 4, 2024

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 with the interdeepMerge() to either the context or the global state 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 are 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.
  • Implement the necessary changes in the deepMerge function to support this behavior.

Testing

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

@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 +306
// 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.

@DAreRodz
Copy link
Contributor

Oh, I understand now. 😄

The context currently uses two proxies: one for property inheritance (proxifyContext) and a second for property reactiveness (proxifyState). To work correctly, the deepMerge function should be used with the second proxy (proxifyState). If not, it doesn't recognize the underlying object, and all property manipulations go through the context/state proxy handlers when it shouldn't

This is what we do in the data-wp-context directive:

  1. Creates a state object (with proxifyState)
    const currentValue = useRef( proxifyState( ns, {} ) );
  2. Updates the state object (with deepMerge)
    deepMerge(
    currentValue.current,
    deepClone( value ) as object,
    false
    );
  3. Wraps the state object with a context proxy (with proxifyContext)
    result[ namespace ] = proxifyContext(
    currentValue.current,
    inheritedValue[ namespace ]
    );

In 48433fe, I moved one of the tests to tests/deep-merge.ts and reproduced the issue.

FAIL packages/interactivity/src/proxies/test/deep-merge.ts
  ● Interactivity API › deepMerge › should handle deeply nested properties that are initially undefined and merged with deepMerge

    expect(jest.fn()).toHaveBeenCalledTimes(expected)

    Expected number of calls: 2
    Received number of calls: 1

      408 |
      409 | 			// The effect should be called again
    > 410 | 			expect( spy ).toHaveBeenCalledTimes( 2 );
          | 			              ^
      411 | 			expect( deepValue ).toBe( 'test value' );
      412 |
      413 | 			// Reading the value directly should also work

      at Object.toHaveBeenCalledTimes (packages/interactivity/src/proxies/test/deep-merge.ts:410:18)

@DAreRodz DAreRodz closed this Sep 23, 2024
@DAreRodz DAreRodz reopened this Sep 23, 2024
Copy link

github-actions bot commented Sep 23, 2024

Flaky tests detected in 8f7fc13.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/11013548741
📝 Reported issues:

@DAreRodz DAreRodz force-pushed the fix/iapi-merging-nested-context branch from 48433fe to 8f7fc13 Compare September 24, 2024 12:15
@DAreRodz DAreRodz changed the title Interactivity API: Correctly handle lazily added, deeply nested properties in context Interactivity API: Correctly handle lazily added, deeply nested properties with deepMerge() Oct 3, 2024
@DAreRodz DAreRodz marked this pull request as ready for review October 3, 2024 12:55
Copy link

github-actions bot commented Oct 3, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: michalczaplinski <czapla@git.wordpress.org>
Co-authored-by: DAreRodz <darerodz@git.wordpress.org>
Co-authored-by: cbravobernal <cbravobernal@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Copy link
Contributor

@cbravobernal cbravobernal left a comment

Choose a reason for hiding this comment

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

I just added the changelog changes. The rest looks good to me.

@cbravobernal cbravobernal force-pushed the fix/iapi-merging-nested-context branch from 2a9bc08 to 005f42d Compare October 4, 2024 14:54
@cbravobernal cbravobernal enabled auto-merge (squash) October 4, 2024 14:55
@cbravobernal cbravobernal merged commit 366cf1f into trunk Oct 4, 2024
62 checks passed
@cbravobernal cbravobernal deleted the fix/iapi-merging-nested-context branch October 4, 2024 15:30
@github-actions github-actions bot added this to the Gutenberg 19.5 milestone Oct 4, 2024
Copy link

github-actions bot commented Oct 4, 2024

There was a conflict while trying to cherry-pick the commit to the wp/6.7 branch. Please resolve the conflict manually and create a PR to the wp/6.7 branch.

PRs to wp/6.7 are similar to PRs to trunk, but you should base your PR on the wp/6.7 branch instead of trunk.

# Checkout the wp/6.7 branch instead of trunk.
git checkout wp/6.7
# Create a new branch for your PR.
git checkout -b my-branch
# Cherry-pick the commit.
git cherry-pick 366cf1f8d6a2169217b82a57ee1af8ba4c1f8b42
# Check which files have conflicts.
git status
# Resolve the conflict...
# Add the resolved files to the staging area.
git status
git add .
git cherry-pick --continue
# Push the branch to the repository
git push origin my-branch
# Create a PR and set the base to the wp/6.7 branch.
# See https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request.

@DAreRodz DAreRodz added the Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) label Oct 4, 2024
Copy link

github-actions bot commented Oct 4, 2024

There was a conflict while trying to cherry-pick the commit to the wp/6.7 branch. Please resolve the conflict manually and create a PR to the wp/6.7 branch.

PRs to wp/6.7 are similar to PRs to trunk, but you should base your PR on the wp/6.7 branch instead of trunk.

# Checkout the wp/6.7 branch instead of trunk.
git checkout wp/6.7
# Create a new branch for your PR.
git checkout -b my-branch
# Cherry-pick the commit.
git cherry-pick 366cf1f8d6a2169217b82a57ee1af8ba4c1f8b42
# Check which files have conflicts.
git status
# Resolve the conflict...
# Add the resolved files to the staging area.
git status
git add .
git cherry-pick --continue
# Push the branch to the repository
git push origin my-branch
# Create a PR and set the base to the wp/6.7 branch.
# See https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-base-branch-of-a-pull-request.

@noisysocks noisysocks added Backported to WP Core Pull request that has been successfully merged into WP Core and removed Backport to WP 6.7 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta labels Oct 6, 2024
noisysocks pushed a commit that referenced this pull request Oct 6, 2024
…rties with `deepMerge()` (#65465)

* test: Add case for deeply nested undefined properties in context proxy

Add a test to ensure the context proxy correctly handles and updates
deeply nested properties that are initially undefined.

* Update the test case to use `proxifyState()`

* Add test for deepMerge with nested undefined properties

* Add a failing test in `deep-merge.ts`

* Add another failing test

* Make all the tests pass

* Simplify code

* Fix test case name

* Add one extra test

* Update test in `context-proxy`

* Update changelog

---------

Co-authored-by: michalczaplinski <czapla@git.wordpress.org>
Co-authored-by: DAreRodz <darerodz@git.wordpress.org>
Co-authored-by: cbravobernal <cbravobernal@git.wordpress.org>
cbravobernal added a commit that referenced this pull request Oct 7, 2024
…rties with `deepMerge()` (#65465)

* test: Add case for deeply nested undefined properties in context proxy

Add a test to ensure the context proxy correctly handles and updates
deeply nested properties that are initially undefined.

* Update the test case to use `proxifyState()`

* Add test for deepMerge with nested undefined properties

* Add a failing test in `deep-merge.ts`

* Add another failing test

* Make all the tests pass

* Simplify code

* Fix test case name

* Add one extra test

* Update test in `context-proxy`

* Update changelog

---------

Co-authored-by: michalczaplinski <czapla@git.wordpress.org>
Co-authored-by: DAreRodz <darerodz@git.wordpress.org>
Co-authored-by: cbravobernal <cbravobernal@git.wordpress.org>
@cbravobernal
Copy link
Contributor

I just cherry-picked this PR to the release/19.4 branch to get it included in the next release: dc70023

@cbravobernal cbravobernal removed the Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) label Oct 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Backported to WP Core Pull request that has been successfully merged into WP Core [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.

4 participants