Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions packages/compose/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- `useCopyToClipboard`: Call the `onSuccess` callback even when the trigger node unmounts before the copy resolves ([#78387](https://github.com/WordPress/gutenberg/pull/78387)).
- `useDialog`: Handle Escape via React `onKeyDown` so portaled descendants can stop propagation to prevent the dialog from closing ([#78433](https://github.com/WordPress/gutenberg/pull/78433)).

### Deprecations

- The `pure` HoC now logs a runtime deprecation warning. Use `memo` or `PureComponent` from `@wordpress/element` instead.

## 7.46.0 (2026-05-14)

## 7.45.0 (2026-04-29)
Expand Down
6 changes: 6 additions & 0 deletions packages/compose/src/higher-order/pure/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ComponentType, ComponentClass } from 'react';
*/
import { isShallowEqual } from '@wordpress/is-shallow-equal';
import { Component } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
Expand All @@ -23,6 +24,11 @@ import { createHigherOrderComponent } from '../../utils/create-higher-order-comp
const pure = createHigherOrderComponent( function < Props extends {} >(
WrappedComponent: ComponentType< Props >
): ComponentType< Props > {
deprecated( 'wp.compose.pure', {
since: '7.1',
alternative: 'Use `memo` or `PureComponent` instead',
} );

if ( WrappedComponent.prototype instanceof Component ) {
return class extends ( WrappedComponent as ComponentClass< Props > ) {
shouldComponentUpdate( nextProps: Props, nextState: any ) {
Expand Down
95 changes: 14 additions & 81 deletions packages/compose/src/higher-order/pure/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,34 @@
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { logged } from '@wordpress/deprecated';

/**
* Internal dependencies
*/
import pure from '../';

describe( 'pure', () => {
it( 'functional component should rerender only when props change', () => {
let i = 0;
const MyComp = pure( () => {
return <p data-testid="counter">{ ++i }</p>;
} );
const { rerender } = render( <MyComp /> );

// Updating with same props doesn't rerender.
rerender( <MyComp /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '1' );

// New prop should trigger a rerender.
rerender( <MyComp { ...{ prop: 'a' } } /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );

// Keeping the same prop value should not rerender.
rerender( <MyComp { ...{ prop: 'a' } } /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );

// Changing the prop value should rerender.
rerender( <MyComp { ...{ prop: 'b' } } /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '3' );
afterEach( () => {
for ( const key in logged ) {
delete logged[ key ];
}
} );

it( 'class component should rerender if the props or state change', async () => {
const user = userEvent.setup();
let i = 0;
const MyComp = pure(
class extends Component {
constructor() {
super( ...arguments );
this.state = {
val: '',
};
}
render() {
return (
<>
<p data-testid="counter">{ ++i }</p>
<input
type="text"
value={ this.state.val }
onChange={ ( e ) =>
this.setState( { val: e.target.value } )
}
/>
<input
type="button"
onClick={ () =>
this.setState( { val: this.state.val } )
}
/>
</>
);
}
}
);

const { rerender } = render( <MyComp /> );
it( 'wraps a component and logs a deprecation warning', () => {
const MyComp = pure( () => <p data-testid="content">content</p> );

// Updating with same props doesn't rerender.
rerender( <MyComp /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '1' );
render( <MyComp /> );

// New prop should trigger a rerender.
rerender( <MyComp { ...{ prop: 'a' } } /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );

// Keeping the same prop value should not rerender.
rerender( <MyComp { ...{ prop: 'a' } } /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );

// Changing the prop value should rerender.
rerender( <MyComp { ...{ prop: 'b' } } /> );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '3' );

// New state value should trigger a rerender.
await user.type( screen.getByRole( 'textbox' ), 'a' );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '4' );

// Keeping the same state value should not trigger a rerender.
await user.click( screen.getByRole( 'button' ) );
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '4' );
expect( console ).toHaveWarnedWith(
'wp.compose.pure is deprecated since version 7.1. Please use Use `memo` or `PureComponent` instead instead.'
);
expect( screen.getByTestId( 'content' ) ).toHaveTextContent(
'content'
);
} );
} );
5 changes: 3 additions & 2 deletions packages/data/src/components/with-select/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* WordPress dependencies
*/
import { createHigherOrderComponent, pure } from '@wordpress/compose';
import { createHigherOrderComponent } from '@wordpress/compose';
import { memo } from '@wordpress/element';

/**
* Internal dependencies
Expand Down Expand Up @@ -58,7 +59,7 @@ const withSelect = (
) =>
createHigherOrderComponent(
( WrappedComponent ) =>
pure( ( ownProps: Record< string, unknown > ) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As a drive-by change, we should give a name to the arrow function. We probably see a lot of Anonymous components in the React dev tools.

memo( ( ownProps: Record< string, unknown > ) => {
const mapSelect = (
select: SelectFunction,
registry: DataRegistry
Expand Down
32 changes: 31 additions & 1 deletion packages/data/src/components/with-select/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import userEvent from '@testing-library/user-event';
* WordPress dependencies
*/
import { compose } from '@wordpress/compose';
import { Component } from '@wordpress/element';
import { Component, createRef } from '@wordpress/element';

/**
* Internal dependencies
Expand Down Expand Up @@ -626,5 +626,35 @@ describe( 'withSelect', () => {
expect( OriginalComponent ).toHaveBeenCalledTimes( 2 );
expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'second' );
} );

it( 'forwards refs to a function component wrapped with withSelect', () => {
const registry = createRegistry();
registry.registerStore( 'demo', {
reducer: ( state = 'value' ) => state,
selectors: {
getValue: ( state ) => state,
},
} );

const OriginalComponent = ( { value, ref } ) => (
<div ref={ ref } role="status">
{ value }
</div>
);

const DataBoundComponent = withSelect( ( _select ) => ( {
value: _select( 'demo' ).getValue(),
} ) )( OriginalComponent );

const ref = createRef();

render(
<RegistryProvider value={ registry }>
<DataBoundComponent ref={ ref } />
</RegistryProvider>
);

expect( ref.current ).toBe( screen.getByRole( 'status' ) );
} );
} );
/* eslint-enable @wordpress/wp-global-usage */
5 changes: 2 additions & 3 deletions packages/viewport/src/with-viewport-match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import type { ComponentType } from 'react';
/**
* WordPress dependencies
*/
import { createElement } from '@wordpress/element';
import { createElement, memo } from '@wordpress/element';
import {
createHigherOrderComponent,
pure,
useViewportMatch,
} from '@wordpress/compose';

Expand Down Expand Up @@ -78,7 +77,7 @@ const withViewportMatch = ( queries: ViewportQueries ) => {
} );
};

return pure( WrappedWithViewport );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm curious why the pure wrapper was used here in the first place. Vast majority of components are technically pure, but don't use pure or memo. If it's not clearly justified, I'd remove it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The pure was added in PR #18950 as parity carry-over from a prior withSelect-based implementation. Can't find any other reason for it. I think it's safe to remove.

return memo( WrappedWithViewport );
},
'withViewportMatch'
);
Expand Down
Loading