Block Context
Block context allows parent blocks to share data with descendant blocks without prop drilling.
How It Works
- Parent block declares which attributes to expose via
providesContext - Child blocks declare which context values they need via
usesContext - WordPress handles the value passing automatically
Providing Context
In block.json
{
"name": "my-plugin/query-container",
"attributes": {
"queryId": {
"type": "string"
},
"postType": {
"type": "string",
"default": "post"
}
},
"providesContext": {
"my-plugin/queryId": "queryId",
"my-plugin/postType": "postType"
}
}
The format is:
"context-key": "attribute-name"
Namespace Your Context Keys
Always namespace context keys to avoid conflicts:
{
"providesContext": {
"my-plugin/recordId": "recordId",
"my-plugin/layout": "layout"
}
}
Consuming Context
In block.json
{
"name": "my-plugin/query-item",
"usesContext": [ "my-plugin/queryId", "my-plugin/postType" ]
}
In JavaScript
Context is passed to the edit function:
export default function Edit( { context } ) {
const queryId = context[ 'my-plugin/queryId' ];
const postType = context[ 'my-plugin/postType' ];
return (
<div { ...useBlockProps() }>
<p>Query ID: { queryId }</p>
<p>Post Type: { postType }</p>
</div>
);
}
In PHP (Dynamic Blocks)
Context is available in the $block parameter:
// render.php
$query_id = $block->context['my-plugin/queryId'] ?? '';
$post_type = $block->context['my-plugin/postType'] ?? 'post';
Or in render_callback:
function render_query_item( $attributes, $content, $block ) {
$query_id = $block->context['my-plugin/queryId'] ?? '';
$post_type = $block->context['my-plugin/postType'] ?? 'post';
return sprintf(
'<div class="query-item" data-query="%s">%s</div>',
esc_attr( $query_id ),
esc_html( $post_type )
);
}
Core Context Values
WordPress core provides these context values:
| Context Key | Provided By | Description |
|---|---|---|
postId |
Query Loop | Current post ID |
postType |
Query Loop | Current post type |
queryId |
Query Loop | Query block instance ID |
query |
Query Loop | Query parameters |
displayLayout |
Query Loop | Layout settings |
templateSlug |
Template | Current template slug |
tagName |
Group | HTML tag name |
commentId |
Comments | Current comment ID |
groupId |
Group | Group block instance ID |
Using Core Context
{
"name": "my-plugin/post-data",
"usesContext": [ "postId", "postType" ]
}
export default function Edit( { context } ) {
const { postId, postType } = context;
// Use post ID to fetch data
const post = useSelect(
( select ) => select( 'core' ).getEntityRecord( 'postType', postType, postId ),
[ postId, postType ]
);
return (
<div { ...useBlockProps() }>
{ post?.title?.rendered }
</div>
);
}
Complete Example: Product Display
Parent Block (Product Container)
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/product",
"title": "Product",
"category": "widgets",
"attributes": {
"productId": {
"type": "number"
},
"showPrice": {
"type": "boolean",
"default": true
},
"currency": {
"type": "string",
"default": "USD"
}
},
"providesContext": {
"my-plugin/productId": "productId",
"my-plugin/showPrice": "showPrice",
"my-plugin/currency": "currency"
},
"editorScript": "file:./index.js"
}
// edit.js
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
const TEMPLATE = [
[ 'my-plugin/product-title' ],
[ 'my-plugin/product-image' ],
[ 'my-plugin/product-price' ],
[ 'my-plugin/product-description' ],
];
const ALLOWED = [
'my-plugin/product-title',
'my-plugin/product-image',
'my-plugin/product-price',
'my-plugin/product-description',
'my-plugin/product-add-to-cart',
];
export default function Edit( { attributes, setAttributes } ) {
const { productId } = attributes;
return (
<div { ...useBlockProps() }>
<TextControl
label="Product ID"
type="number"
value={ productId }
onChange={ ( value ) => setAttributes( { productId: parseInt( value ) } ) }
/>
<InnerBlocks
template={ TEMPLATE }
allowedBlocks={ ALLOWED }
/>
</div>
);
}
Child Block (Product Price)
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/product-price",
"title": "Product Price",
"category": "widgets",
"parent": [ "my-plugin/product" ],
"usesContext": [ "my-plugin/productId", "my-plugin/showPrice", "my-plugin/currency" ],
"editorScript": "file:./index.js",
"render": "file:./render.php"
}
// edit.js
export default function Edit( { context } ) {
const productId = context[ 'my-plugin/productId' ];
const showPrice = context[ 'my-plugin/showPrice' ];
const currency = context[ 'my-plugin/currency' ];
if ( ! showPrice ) {
return null;
}
// In real implementation, fetch price from API
const price = 99.99;
return (
<div { ...useBlockProps() }>
<span className="price">
{ currency } { price }
</span>
</div>
);
}
<?php
// render.php
$product_id = $block->context['my-plugin/productId'] ?? 0;
$show_price = $block->context['my-plugin/showPrice'] ?? true;
$currency = $block->context['my-plugin/currency'] ?? 'USD';
if ( ! $show_price || ! $product_id ) {
return '';
}
// In real implementation, get price from database
$price = get_post_meta( $product_id, '_price', true );
$wrapper_attributes = get_block_wrapper_attributes();
?>
<div <?php echo $wrapper_attributes; ?>>
<span class="price">
<?php echo esc_html( $currency ); ?>
<?php echo esc_html( number_format( $price, 2 ) ); ?>
</span>
</div>
Context with Query Loop
Using context with WordPress Query Loop:
{
"name": "my-plugin/post-reading-time",
"title": "Reading Time",
"usesContext": [ "postId" ]
}
<?php
// render.php
$post_id = $block->context['postId'] ?? get_the_ID();
if ( ! $post_id ) {
return '';
}
$content = get_post_field( 'post_content', $post_id );
$word_count = str_word_count( wp_strip_all_tags( $content ) );
$reading_time = ceil( $word_count / 200 ); // 200 words per minute
$wrapper_attributes = get_block_wrapper_attributes();
?>
<div <?php echo $wrapper_attributes; ?>>
<?php printf( esc_html__( '%d min read', 'my-plugin' ), $reading_time ); ?>
</div>
Dynamic Context Values
Context values update when parent attributes change:
// Parent block
export default function Edit( { attributes, setAttributes } ) {
const { layout } = attributes;
return (
<div { ...useBlockProps() }>
<ToggleControl
label="Grid Layout"
checked={ layout === 'grid' }
onChange={ () => setAttributes( {
layout: layout === 'grid' ? 'list' : 'grid'
} ) }
/>
<InnerBlocks />
</div>
);
}
// Child block automatically receives updated layout value
export default function ChildEdit( { context } ) {
const layout = context[ 'my-plugin/layout' ];
// Automatically re-renders when parent changes layout
return (
<div { ...useBlockProps( { className: `layout-${ layout }` } ) }>
Current layout: { layout }
</div>
);
}
Context Inheritance Depth
Context passes through unlimited nesting levels:
Parent (provides context)
└── Child A
└── Child B
└── Child C (can still access Parent's context)
Any descendant can consume context from any ancestor.
Overriding Context
A block can both consume and provide the same context:
{
"name": "my-plugin/nested-container",
"usesContext": [ "my-plugin/level" ],
"providesContext": {
"my-plugin/level": "level"
},
"attributes": {
"level": {
"type": "number",
"default": 1
}
}
}
export default function Edit( { attributes, setAttributes, context } ) {
const parentLevel = context[ 'my-plugin/level' ] || 0;
// Auto-set level based on parent
useEffect( () => {
setAttributes( { level: parentLevel + 1 } );
}, [ parentLevel ] );
return (
<div { ...useBlockProps() }>
<p>Level: { attributes.level }</p>
<InnerBlocks />
</div>
);
}
Best Practices
-
Namespace context keys: Always use
plugin-name/context-keyformat. -
Provide defaults: Handle missing context gracefully.
-
Limit context scope: Only expose what child blocks actually need.
-
Document context: Clearly document what context your block provides/consumes.
-
Consider performance: Context changes re-render all consuming descendants.
-
Use for configuration: Context is ideal for settings that affect multiple child blocks.
-
Combine with parent restriction: Use
parentin block.json to ensure context is available.