Skip to content

Commit caae289

Browse files
Mamadukajsnajdr
andauthored
Compose: Fully deprecate the 'pure' HoC (#78674)
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org> Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
1 parent f5c3dd6 commit caae289

6 files changed

Lines changed: 59 additions & 88 deletions

File tree

packages/compose/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
- `useCopyToClipboard`: Call the `onSuccess` callback even when the trigger node unmounts before the copy resolves ([#78387](https://github.com/WordPress/gutenberg/pull/78387)).
1212
- `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)).
1313

14+
### Deprecations
15+
16+
- The `pure` HoC now logs a runtime deprecation warning. Use `memo` or `PureComponent` from `@wordpress/element` instead.
17+
1418
## 7.46.0 (2026-05-14)
1519

1620
## 7.45.0 (2026-04-29)

packages/compose/src/higher-order/pure/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { ComponentType, ComponentClass } from 'react';
88
*/
99
import { isShallowEqual } from '@wordpress/is-shallow-equal';
1010
import { Component } from '@wordpress/element';
11+
import deprecated from '@wordpress/deprecated';
1112

1213
/**
1314
* Internal dependencies
@@ -23,6 +24,11 @@ import { createHigherOrderComponent } from '../../utils/create-higher-order-comp
2324
const pure = createHigherOrderComponent( function < Props extends {} >(
2425
WrappedComponent: ComponentType< Props >
2526
): ComponentType< Props > {
27+
deprecated( 'wp.compose.pure', {
28+
since: '7.1',
29+
alternative: 'Use `memo` or `PureComponent` instead',
30+
} );
31+
2632
if ( WrappedComponent.prototype instanceof Component ) {
2733
return class extends ( WrappedComponent as ComponentClass< Props > ) {
2834
shouldComponentUpdate( nextProps: Props, nextState: any ) {

packages/compose/src/higher-order/pure/test/index.js

Lines changed: 14 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,101 +2,34 @@
22
* External dependencies
33
*/
44
import { render, screen } from '@testing-library/react';
5-
import userEvent from '@testing-library/user-event';
65

76
/**
87
* WordPress dependencies
98
*/
10-
import { Component } from '@wordpress/element';
9+
import { logged } from '@wordpress/deprecated';
1110

1211
/**
1312
* Internal dependencies
1413
*/
1514
import pure from '../';
1615

1716
describe( 'pure', () => {
18-
it( 'functional component should rerender only when props change', () => {
19-
let i = 0;
20-
const MyComp = pure( () => {
21-
return <p data-testid="counter">{ ++i }</p>;
22-
} );
23-
const { rerender } = render( <MyComp /> );
24-
25-
// Updating with same props doesn't rerender.
26-
rerender( <MyComp /> );
27-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '1' );
28-
29-
// New prop should trigger a rerender.
30-
rerender( <MyComp { ...{ prop: 'a' } } /> );
31-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );
32-
33-
// Keeping the same prop value should not rerender.
34-
rerender( <MyComp { ...{ prop: 'a' } } /> );
35-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );
36-
37-
// Changing the prop value should rerender.
38-
rerender( <MyComp { ...{ prop: 'b' } } /> );
39-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '3' );
17+
afterEach( () => {
18+
for ( const key in logged ) {
19+
delete logged[ key ];
20+
}
4021
} );
4122

42-
it( 'class component should rerender if the props or state change', async () => {
43-
const user = userEvent.setup();
44-
let i = 0;
45-
const MyComp = pure(
46-
class extends Component {
47-
constructor() {
48-
super( ...arguments );
49-
this.state = {
50-
val: '',
51-
};
52-
}
53-
render() {
54-
return (
55-
<>
56-
<p data-testid="counter">{ ++i }</p>
57-
<input
58-
type="text"
59-
value={ this.state.val }
60-
onChange={ ( e ) =>
61-
this.setState( { val: e.target.value } )
62-
}
63-
/>
64-
<input
65-
type="button"
66-
onClick={ () =>
67-
this.setState( { val: this.state.val } )
68-
}
69-
/>
70-
</>
71-
);
72-
}
73-
}
74-
);
75-
76-
const { rerender } = render( <MyComp /> );
23+
it( 'wraps a component and logs a deprecation warning', () => {
24+
const MyComp = pure( () => <p data-testid="content">content</p> );
7725

78-
// Updating with same props doesn't rerender.
79-
rerender( <MyComp /> );
80-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '1' );
26+
render( <MyComp /> );
8127

82-
// New prop should trigger a rerender.
83-
rerender( <MyComp { ...{ prop: 'a' } } /> );
84-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );
85-
86-
// Keeping the same prop value should not rerender.
87-
rerender( <MyComp { ...{ prop: 'a' } } /> );
88-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '2' );
89-
90-
// Changing the prop value should rerender.
91-
rerender( <MyComp { ...{ prop: 'b' } } /> );
92-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '3' );
93-
94-
// New state value should trigger a rerender.
95-
await user.type( screen.getByRole( 'textbox' ), 'a' );
96-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '4' );
97-
98-
// Keeping the same state value should not trigger a rerender.
99-
await user.click( screen.getByRole( 'button' ) );
100-
expect( screen.getByTestId( 'counter' ) ).toHaveTextContent( '4' );
28+
expect( console ).toHaveWarnedWith(
29+
'wp.compose.pure is deprecated since version 7.1. Please use Use `memo` or `PureComponent` instead instead.'
30+
);
31+
expect( screen.getByTestId( 'content' ) ).toHaveTextContent(
32+
'content'
33+
);
10134
} );
10235
} );

packages/data/src/components/with-select/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/**
22
* WordPress dependencies
33
*/
4-
import { createHigherOrderComponent, pure } from '@wordpress/compose';
4+
import { createHigherOrderComponent } from '@wordpress/compose';
5+
import { memo } from '@wordpress/element';
56

67
/**
78
* Internal dependencies
@@ -58,7 +59,7 @@ const withSelect = (
5859
) =>
5960
createHigherOrderComponent(
6061
( WrappedComponent ) =>
61-
pure( ( ownProps: Record< string, unknown > ) => {
62+
memo( function WithSelect( ownProps: Record< string, unknown > ) {
6263
const mapSelect = (
6364
select: SelectFunction,
6465
registry: DataRegistry

packages/data/src/components/with-select/test/index.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import userEvent from '@testing-library/user-event';
88
* WordPress dependencies
99
*/
1010
import { compose } from '@wordpress/compose';
11-
import { Component } from '@wordpress/element';
11+
import { Component, createRef } from '@wordpress/element';
1212

1313
/**
1414
* Internal dependencies
@@ -626,5 +626,35 @@ describe( 'withSelect', () => {
626626
expect( OriginalComponent ).toHaveBeenCalledTimes( 2 );
627627
expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'second' );
628628
} );
629+
630+
it( 'forwards refs to a function component wrapped with withSelect', () => {
631+
const registry = createRegistry();
632+
registry.registerStore( 'demo', {
633+
reducer: ( state = 'value' ) => state,
634+
selectors: {
635+
getValue: ( state ) => state,
636+
},
637+
} );
638+
639+
const OriginalComponent = ( { value, ref } ) => (
640+
<div ref={ ref } role="status">
641+
{ value }
642+
</div>
643+
);
644+
645+
const DataBoundComponent = withSelect( ( _select ) => ( {
646+
value: _select( 'demo' ).getValue(),
647+
} ) )( OriginalComponent );
648+
649+
const ref = createRef();
650+
651+
render(
652+
<RegistryProvider value={ registry }>
653+
<DataBoundComponent ref={ ref } />
654+
</RegistryProvider>
655+
);
656+
657+
expect( ref.current ).toBe( screen.getByRole( 'status' ) );
658+
} );
629659
} );
630660
/* eslint-enable @wordpress/wp-global-usage */

packages/viewport/src/with-viewport-match.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type { ComponentType } from 'react';
99
import { createElement } from '@wordpress/element';
1010
import {
1111
createHigherOrderComponent,
12-
pure,
1312
useViewportMatch,
1413
} from '@wordpress/compose';
1514

@@ -70,15 +69,13 @@ const withViewportMatch = ( queries: ViewportQueries ) => {
7069
< T extends Record< string, unknown > >(
7170
WrappedComponent: ComponentType< T >
7271
) => {
73-
const WrappedWithViewport = ( props: T ) => {
72+
return function WithViewportMatch( props: T ) {
7473
const queriesResult = useViewPortQueriesResult();
7574
return createElement( WrappedComponent, {
7675
...props,
7776
...queriesResult,
7877
} );
7978
};
80-
81-
return pure( WrappedWithViewport );
8279
},
8380
'withViewportMatch'
8481
);

0 commit comments

Comments
 (0)