Application state managed withDispatch, withSelect and compose 101


Editor related Javascript code is full of references to withDispatch, withSelect and compose. This article briefly explains why these methods are important, what they’re used for and how to use them.

withDispatch, withSelect and compose are a triad of Gutenberg methods that are often used together to manage application state. They are how Gutenberg component and blocks read and modify editor data from the outside of the component.

You should know that useSelect and useDispatch hooks are a more modern way of managing application state but the with* methods are widespread across Gutenberg and custom editor code, and likely will be for sometime, so being able to read them is worthwhile. The main advantage of the use* hooks is that they make for more readable code. It’s relatively straight-forward to switch to the modern use* hooks rather than the with* methods.

withDispatch

You pass withDispatch a function which receives a dispatch method. The purpose of the function is to return a list of dispatch (data modification) methods for the components use.

withDispatch( dispatch => {    
	return { replaceBlock, selectBlock } = dispatch( 'core/block-editor' );
} )

In the above example it’s returning the replaceBlock and selectBlock methods from the core/block-editor data store. They will be passed on as a prop to your component, so you can use them when you need to change state.

The methods returned are just methods, they don’t need to be existing ones but could be wrappers around them, like in this example below from the Jetpack recurring-payments block.

withDispatch( ( dispatch, props ) => ( {    
	/**     
	 * Updates the plan on the Recurring Payments block acting as a subscribe button.        
	 * @param {number} planId - Plan ID.     
	 */    
	setSubscribeButtonPlan( planId ) {        
		dispatch( 'core/block-editor' ).updateBlockAttributes(
			props.subscribeButton.clientId,
			{ planId }
		);      
	},
} ) ),

In the browser dev console it is possible to use wp.data.dispatch( store_name ) directly for debugging purposes.

withSelect

You pass withSelect a function which receives a select method. The purpose of the function is to return a list of select (data query) values that the component needs.

withSelect( ( select ) => ( {
	postId: select( 'core/editor' ).getCurrentPostId(),
} ) );

In the above example it’s returning the postId of the post currently being edited, it gets this from the core/editor store.

The postId will be passed on to the component as a prop. The value of the prop will then automatically be changed inside the component as the value returned by the selector changes.

In the browser dev console it is possible to use wp.data.select( store_name ) directly for debugging purposes.

compose

compose is passed an array of methods, it runs each of these methods in turn starting from the right. The result of each method is passed into the next method. The below two JavaScript snippets are identical.

compose( [ methodOne, methodTwo, methodThree ] ) ()
methodOne( methodTwo( methodThree() ) )

withSelect and withDispatch are usually called within the compose method like this:

// Where your_component_class_or_function is a standard class/function receiving props
export default compose ( [
	withSelect( the_with_select_method ),   
	withDispatch( the_withDispatch_method )
] ) ( your_component_class_or_fn )

The component you pass then will have the values returned from the methods passed to withSelect and withDispatch available as props.

Comparing use and with methods

use* hooks are a more modern way of achieving the same thing, they are usually preferred to with* methods because they are more succinct and it easier to read. It’s easier to read because use* places the definition of the values next to where the values are used, whereas with* places the use of the values inside the component and the definition of the values at the bottom of the file, outside of the component.

In addition, the compose method is no longer needed at all. Rather than passing the values and functions as props via compose, the values and methods are available within the use of the hook.

Below is a brief made-up example to illustrate the differences between the two approaches.

with*

function MyAwesomeComponent( {
	// The values from the with* methods are passed as props
	clientId,
	defaultVariation,
	hasInnerBlocks,
	replaceBlock,
	selectBlock
} ) {
	// A made-up example using the props.
	const replaceEmptyBlock = () => {
		if ( ! hasInnerBlocks && ‘flibble/baz’ === defaultVariation ) {
			replaceBlock( clientId, ‘flibble/block’ );
			selectBlock( clientId );
		}
	}
}
export default compose ( [
	withSelect( (select, props ) => {
		const { getDefaultBlockVariation } = select( blocksStore );
		const { getBlocks } = select( blockEditorStore );
		return {
			defaultVariation: getDefaultBlockVariation( props.name );
			hasInnerBlocks: getBlocks( props.clientId )?.length > 0,
		};
	} ),
	withDispatch( ( select ) => {
		const { replaceBlock, selectBlock } = dispatch( blockEditorStore );
		return { replaceBlock, selectBlock }
	} )
] )( MyAwesomeComponent );

use*

export function MyAwesomeComponent( { name, clientId } ) {
	// The values from useSelect are fetched and are immediately available rather than being passed as props
	const { defaultVariation, hasInnerBlocks } = useSelect( select => {
		const { getDefaultBlockVariation } = select( blocksStore );
		const { getBlocks } = select( blockEditorStore );
		return {
			defaultVariation: getDefaultBlockVariation( name ),
			hasInnerBlocks: getBlocks( clientId )?.length > 0,
		};
	}, [ name, clientId ] );

	const { replaceBlock, selectBlock } = useDispatch( blockEditorStore );

	// A made-up example using the props.
	const replaceEmptyBlock = () => {
		if ( ! hasInnerBlocks && ‘flibble/baz’ === defaultVariation ) {
			replaceBlock( clientId, ‘flibble/quux’ );
			selectBlock( clientId );
		}
	}
}

One other difference you might have noticed is that [ name, clientId ] is passed to useSelect as the second parameter. These are variables come from outside of useSelect but are used inside the first parameter, passing them as the second parameter means that useSelect will use them to memoize (cache) the first parameter.

Further reading

Don’t miss new posts on this site. Subscribe.

With thanks to @bph, Milana Cap and Marco Ciampini for reviewing earlier drafts of this article. All errors are mine.

Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *