Skip to content

Jetpack: WordPress 6.8 Compatibility - Fix data manipulation within useSelect in core features #42435

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

Merged
Merged
Show file tree
Hide file tree
Changes from 9 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
@@ -0,0 +1,4 @@
Significance: patch
Type: other

Compatibility: Ensuring performance best practices and reducing console warnings.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import apiFetch from '@wordpress/api-fetch';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useRef } from '@wordpress/element';
import { useEffect, useRef, useMemo } from '@wordpress/element';
import { escapeHTML } from '@wordpress/escape-html';

// Tries to create a tag or fetch it if it already exists.
Expand Down Expand Up @@ -31,30 +31,59 @@ export function usePromptTags( promptId, tagsAdded, setTagsAdded ) {
// Statuses are 'not-started', 'pending', 'fulfilled', or 'rejected'.
const promptTagRequestStatus = useRef( 'not-started' );

// Get information about tags for the edited post.
const { postType, postsSupportTags, tags, tagIds, tagsHaveResolved } = useSelect( select => {
const { getEditedPostAttribute } = select( 'core/editor' );
const { getEntityRecords, getPostType, hasFinishedResolution } = select( 'core' );
const _termIds = getEditedPostAttribute( 'tags' );
// Split into separate selectors to maintain referential equality
const postType = useSelect(
select => select( 'core/editor' ).getEditedPostAttribute( 'type' ),
[]
);

const query = {
const tagIds = useSelect(
select => select( 'core/editor' ).getEditedPostAttribute( 'tags' ) || [],
[]
);

const postsSupportTags = useSelect(
select => select( 'core' ).getPostType( 'post' )?.taxonomies?.includes( 'post_tag' ),
[]
);

// Memoize the query object
const query = useMemo( () => {
if ( tagIds.length === 0 ) {
return null;
}
return {
_fields: 'id,name',
context: 'view',
include: _termIds?.join( ',' ),
include: tagIds.join( ',' ),
per_page: -1,
};
}, [ tagIds ] );

return {
postType: getEditedPostAttribute( 'type' ),
postsSupportTags: getPostType( 'post' )?.taxonomies.includes( 'post_tag' ),
tagIds: _termIds || [],
tags: _termIds && _termIds.length ? getEntityRecords( 'taxonomy', 'post_tag', query ) : [],
tagsHaveResolved:
_termIds && _termIds.length
? hasFinishedResolution( 'getEntityRecords', [ 'taxonomy', 'post_tag', query ] )
: true,
};
}, [] );
// Get tags with memoized query
const tags = useSelect(
select => {
if ( tagIds.length === 0 ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could that condition be moved out of the function passed to useSelect? Or would that create an issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, useSelect itself can't be called conditionally. I believe we could miss important store updates when switching between states as well if we were able to use it conditionally.

return [];
}
return select( 'core' ).getEntityRecords( 'taxonomy', 'post_tag', query ) || [];
},
[ query, tagIds.length ]
);

const tagsHaveResolved = useSelect(
select => {
if ( tagIds.length === 0 ) {
return true;
}
return select( 'core' ).hasFinishedResolution( 'getEntityRecords', [
'taxonomy',
'post_tag',
query,
] );
},
[ query, tagIds.length ]
);

// Add the related prompt tags, if we're able and they haven't been added already.
useEffect( () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { isEmpty, mapValues, pickBy } from 'lodash';
import { isEmpty } from 'lodash';

export default function usePassthroughAttributes( { attributes, clientId, setAttributes } ) {
// `passthroughAttributes` is a map of child/parent attribute names,
// to indicate which parent attribute values will be injected into which child attributes.
// E.g. { childAttribute: 'parentAttribute' }
const { passthroughAttributes } = attributes;
const attributesToSync = useSelect(
select => {
const { getBlockAttributes, getBlockRootClientId } = select( 'core/block-editor' );
const parentClientId = getBlockRootClientId( clientId );
const parentAttributes = getBlockAttributes( parentClientId ) || {};

const { attributesToSync } = useSelect( select => {
const { getBlockAttributes, getBlockRootClientId } = select( 'core/block-editor' );
// Check the root block from which the block is nested, that is, the immediate parent.
const parentClientId = getBlockRootClientId( clientId );
const parentAttributes = getBlockAttributes( parentClientId ) || {};
// Here we actually map the parent attribute value to the child attribute.
// E.g. { childAttribute: 'foobar' }
const mappedAttributes = mapValues( passthroughAttributes, key => parentAttributes[ key ] );
// Only compute if we have both parent attributes and passthrough mappings
if ( ! parentClientId || ! passthroughAttributes ) {
return {};
}

// Discard all equals attributes
const validAttributes = pickBy(
mappedAttributes,
( value, key ) => value !== attributes[ key ]
);
// Create new object only if values are different
let hasChanges = false;
const validAttributes = {};

return { attributesToSync: validAttributes };
} );
// Check each attribute mapping
Object.entries( passthroughAttributes ).forEach( ( [ childKey, parentKey ] ) => {
const parentValue = parentAttributes[ parentKey ];
if ( parentValue !== attributes[ childKey ] ) {
hasChanges = true;
validAttributes[ childKey ] = parentValue;
}
} );

return hasChanges ? validAttributes : {};
},
[ clientId, passthroughAttributes, attributes ]
);

useEffect( () => {
if ( ! isEmpty( attributesToSync ) ) {
Expand Down
27 changes: 24 additions & 3 deletions projects/plugins/jetpack/extensions/blocks/slideshow/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,35 @@ export const SlideshowEdit = ( {
);
};

const memoCache = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering whether we should clear the map somewhere, maybe when SlideshowEdit unmounts. But that may be overkill.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good thought, thank you. I looked into this more, and from what I understand it would be overkill based on page refresh that should clean this up so the cache is per session, and that the data is lightweight (only image id and size information, not remaining image information). And lastly in attempting to add this it was turning into a complex operation as well.
As for page refresh cleaning the data up, I could confirm this by adding window.debugMemoCache = memoCache; to the code, then logging this in the browser console before adding a block with images, and then after a page refresh. After the page refresh it defaults back to a size of 1. And if I remove the Slideshow ad refresh, the cache is gone.
So based on that I think most likely overkill here, would you agree?

Copy link
Contributor

Choose a reason for hiding this comment

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

Absolutely! Thanks for looking it up.


export default compose(
withSelect( ( select, props ) => {
const imageSizes = select( 'core/editor' ).getEditorSettings().imageSizes;
const resizedImages = props.attributes.ids.reduce( ( currentResizedImages, id ) => {
const image = select( 'core' ).getMedia( id );
const { getEditorSettings } = select( 'core/editor' );
const { ids } = props.attributes;

const imageSizes = getEditorSettings().imageSizes;

// Create cache key
const memoKey = ids.join( ',' );

if ( memoCache.has( memoKey ) ) {
return {
imageSizes,
resizedImages: memoCache.get( memoKey ),
};
}

// If not in cache, calculate new value
const { getMedia } = select( 'core' );
const resizedImages = ids.reduce( ( currentResizedImages, id ) => {
const image = getMedia( id );
const sizes = get( image, [ 'media_details', 'sizes' ] );
return [ ...currentResizedImages, { id, sizes } ];
}, [] );

memoCache.set( memoKey, resizedImages );

return {
imageSizes,
resizedImages,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,10 @@ function NewProduct( { onClose } ) {
export default function ProductManagementToolbarControl() {
const { products, productType, selectedProductIds } = useProductManagementContext();

const { selectedProducts } = useSelect( select => {
const { getSelectedProducts } = select( membershipProductsStore );
return {
selectedProducts: getSelectedProducts( selectedProductIds ),
};
} );
const selectedProducts = useSelect(
select => select( membershipProductsStore ).getSelectedProducts( selectedProductIds ),
[ selectedProductIds ]
);

let productDescription = null;
let subscriptionIcon = update;
Expand Down
Loading