Categories
Software Development

WordPress Blocks: React Context Cuts Clutter

Using Reacts’ Context in WordPress blocks passes parameters painlessly. Attributes carried by context keep coupling minimal.

Reacts’ Context feature facilitates distributing data down a component tree to any descendant component. Instead of passing attributes between your components manually (e.g. <FooComponent attrib={ attrib }>), which tightly couples the current attributes and components, using a block-level context makes it easy to update and manage attributes (and their changes).

WordPress Blocks (a.k.a. Gutenberg) supports a similar concept called Block Context, but it’s for communicating and sharing attributes between discrete blocks (those registered using registerBlockType).

To someone familiar with React the following is probably obvious and straight-forward, but I found implementing it non-trivial, and also not covered in the WordPress documentation. Regardless, this appears to be so functional it should be a best practice.

Example:

A WordPress block has at minimum 2 top-level components, an Edit and a Save. Each component is passed the block attributes in the first argument. Typically the save component is less-interesting as it specifies the block data saved in the post-content, but if required it would function identically to the following example used with the edit component.

For this example let’s examine a block with 1 attribute, passed to both the toolbar and the sidebar.

export default function Edit( props ) {
	const { foo = '' } = props.attributes;
	const blockProps = useBlockProps();

	const updateFoo = useCallback(
		( _foo ) => {
			props.setAttributes( { foo: _foo } );
		},
		[ foo ]
	);

	return (
		<div { ...blockProps }>
			<MyPluginBlockControls foo={foo} onFooChange={updateFoo}></MyPluginBlockControls>
			<MyPluginInspectorControls foo={foo} onFooChange={updateFoo}></MyPluginInspectorControls>
			...
		</div>
	);
}

While this example is a bit contrived (with only one attribute it is probably not necessary to create subcomponents managing our controls), it should be evident that adding additional attributes cascades changes through the tree. Using a Context instead would resemble code like (differences are highlighted):

import { FooContext } from './context';

export default function Edit( props ) {
	const { foo = '' } = props.attributes;
	const blockProps = useBlockProps();

	const updateFoo = useCallback(
		( _foo ) => {
			props.setAttributes( { foo: _foo } );
		},
		[ foo ]
	);

	const blockContext = {
		foo,
		updateFoo,
	};

	return (
		<FooContext.Provider value={ blockContext }>
			<div { ...blockProps }>
				<MyPluginBlockControls></MyPluginBlockControls>
				<MyPluginInspectorControls></MyPluginInspectorControls>
				...
			</div>
		<FooContext.Provider/>
	);
}

The Context

Creating the context itself is pretty simple:

import { createContext, useContext } from '@wordpress/element';

export const FooContext = createContext( {
	foo: undefined,
	updateFoo: () => {},
} );

export const useFooContext = () => useContext( FooContext );

Using the Context in a sub-component

import { useFooContext } from './context';

export function MyPluginBlockControls() {
	// Use `foo` as normal.  If you need to write it, you can also add `updateFoo`.
	const { foo } = useFooContext();
}

Adding additional attributes

If you modify your block.json to have additional attributes it’s simple to add them to the context (use the property attribute and create a write function at the top-level for every attribute ), then it’s trivial to change the components to import the attribute with the const {foo,bar} = useFooContext() syntax.

Conclusion

This pattern appears to be a good way to decouple attributes within a WordPress block that need to be passed either to re-usable components or just to simplify what code should change when adding/updating the attributes being used. I’m still a React novice and I would love to know if there’s a problem or if it could be improved any way — let me know in the comments!