diff --git a/blocks/library/html/index.js b/blocks/library/html/index.js
index 8f32db2e9a026e..70a20e4a7e7ca6 100644
--- a/blocks/library/html/index.js
+++ b/blocks/library/html/index.js
@@ -7,7 +7,7 @@ import TextareaAutosize from 'react-autosize-textarea';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { Component } from '@wordpress/element';
+import { withState } from '@wordpress/components';
/**
* Internal dependencies
@@ -41,60 +41,42 @@ registerBlockType( 'core/html', {
},
},
- edit: class extends Component {
- constructor() {
- super( ...arguments );
- this.preview = this.preview.bind( this );
- this.edit = this.edit.bind( this );
- this.state = {
- preview: false,
- };
- }
-
- preview() {
- this.setState( { preview: true } );
- }
-
- edit() {
- this.setState( { preview: false } );
- }
-
- render() {
- const { preview } = this.state;
- const { attributes, setAttributes, focus } = this.props;
-
- return (
-
- { focus &&
-
-
-
-
-
-
- }
- { preview ?
-
:
-
setAttributes( { content: event.target.value } ) }
- />
- }
- { focus &&
-
-
- { __( 'Add custom HTML code and preview it right here in the editor.' ) }
-
-
- }
+ edit: withState( {
+ preview: false,
+ } )( ( { attributes, setAttributes, setState, focus, preview } ) => [
+ focus && (
+
+
+
+
- );
- }
- },
+
+ ),
+ preview ?
+ :
+ setAttributes( { content: event.target.value } ) }
+ />,
+ focus && (
+
+
+ { __( 'Add custom HTML code and preview it right here in the editor.' ) }
+
+
+ ),
+ ] ),
save( { attributes } ) {
return attributes.content;
diff --git a/components/higher-order/with-state/README.md b/components/higher-order/with-state/README.md
new file mode 100644
index 00000000000000..bcc4e704521c54
--- /dev/null
+++ b/components/higher-order/with-state/README.md
@@ -0,0 +1,32 @@
+withState
+=========
+
+`withState` is a React [higher-order component](https://facebook.github.io/react/docs/higher-order-components.html) which enables a function component to have internal state.
+
+Wrapping a component with `withState` provides state as props to the wrapped component, along with a `setState` updater function.
+
+## Usage
+
+```jsx
+/**
+ * WordPress dependencies
+ */
+import { withState } from '@wordpress/components';
+
+function MyCounter( { count, setState } ) {
+ return (
+ <>
+ Count: { count }
+
+ >
+ );
+}
+
+export default withState( {
+ count: 0,
+} )( MyCounter );
+```
+
+`withState` optionally accepts an object argument to define the initial state. It returns a function which can then be used in composing your component.
diff --git a/components/higher-order/with-state/index.js b/components/higher-order/with-state/index.js
new file mode 100644
index 00000000000000..3e2bea08a32da6
--- /dev/null
+++ b/components/higher-order/with-state/index.js
@@ -0,0 +1,41 @@
+/**
+ * WordPress dependencies
+ */
+import { Component, getWrapperDisplayName } from '@wordpress/element';
+
+/**
+ * A Higher Order Component used to provide and manage internal component state
+ * via props.
+ *
+ * @param {?Object} initialState Optional initial state of the component
+ * @return {Component} Wrapped component
+ */
+function withState( initialState = {} ) {
+ return ( OriginalComponent ) => {
+ class WrappedComponent extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.setState = this.setState.bind( this );
+
+ this.state = initialState;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ WrappedComponent.displayName = getWrapperDisplayName( WrappedComponent, 'state' );
+
+ return WrappedComponent;
+ };
+}
+
+export default withState;
diff --git a/components/higher-order/with-state/test/index.js b/components/higher-order/with-state/test/index.js
new file mode 100644
index 00000000000000..b9187614a26cbe
--- /dev/null
+++ b/components/higher-order/with-state/test/index.js
@@ -0,0 +1,25 @@
+/**
+ * External dependencies
+ */
+import { mount } from 'enzyme';
+
+/**
+ * Internal dependencies
+ */
+import withState from '../';
+
+describe( 'withState', () => {
+ it( 'should pass initial state and allow updates', () => {
+ const EnhancedComponent = withState( { count: 0 } )( ( { count, setState } ) => (
+
+ ) );
+
+ const wrapper = mount( );
+
+ expect( wrapper.html() ).toBe( '' );
+ wrapper.find( 'button' ).simulate( 'click' );
+ expect( wrapper.html() ).toBe( '' );
+ } );
+} );
diff --git a/components/index.js b/components/index.js
index 299c2477f237c1..dc63a3eabae280 100644
--- a/components/index.js
+++ b/components/index.js
@@ -44,3 +44,4 @@ export { default as withFocusOutside } from './higher-order/with-focus-outside';
export { default as withFocusReturn } from './higher-order/with-focus-return';
export { default as withInstanceId } from './higher-order/with-instance-id';
export { default as withSpokenMessages } from './higher-order/with-spoken-messages';
+export { default as withState } from './higher-order/with-state';