How to Build Dynamic Blocks with WordPress Block Editor

Dynamic blocks represent a powerful evolution in WordPress block development, enabling server-side rendering that displays real-time data, database queries, and user-specific content. This guide teaches you to build dynamic blocks that combine the flexibility of Gutenberg with the power of PHP.

Static vs Dynamic Blocks

Understanding the difference between static and dynamic blocks is fundamental.

Static blocks save their content directly to the database as HTML. When you create a paragraph block, WordPress stores the exact HTML in post_content. This is efficient for content that doesn’t change.

Dynamic blocks save only their attributes to the database. Each time the page loads, WordPress executes a PHP render callback that generates the HTML. This enables real-time data like “latest posts,” user-specific content, and database queries.

Use static blocks for fixed content. Use dynamic blocks when content must update automatically based on data changes, user context, or external sources.

When to Use Dynamic Blocks

Dynamic blocks excel in specific scenarios:

  • Latest content displays – Showing recent posts, comments, or custom post types
  • User-specific content – Displaying personalized data based on logged-in users
  • External data – Fetching and displaying API data, weather, stock prices
  • Database queries – Running complex WP_Query operations
  • Real-time calculations – Computing values based on current data

Avoid dynamic blocks for purely presentational content that never changes. The server-side rendering adds processing overhead unnecessary for static content.

Creating Your First Dynamic Block

Start with block.json to define your block:

{
    "apiVersion": 3,
    "name": "dprt/recent-posts",
    "title": "Recent Posts",
    "category": "widgets",
    "icon": "list-view",
    "description": "Display recent posts dynamically",
    "attributes": {
        "numberOfPosts": {
            "type": "number",
            "default": 5
        },
        "postType": {
            "type": "string",
            "default": "post"
        }
    },
    "editorScript": "file:./index.js",
    "editorStyle": "file:./editor.css",
    "style": "file:./style.css"
}

Register the block with a render callback:

function dprt_register_recent_posts_block() {
    register_block_type( __DIR__ . '/build/blocks/recent-posts', array(
        'render_callback' => 'dprt_render_recent_posts_block'
    ) );
}
add_action( 'init', 'dprt_register_recent_posts_block' );

Implementing the Render Callback

The render callback function generates your block’s HTML:

function dprt_render_recent_posts_block( $attributes, $content, $block ) {
    $number_of_posts = isset( $attributes['numberOfPosts'] ) ? absint( $attributes['numberOfPosts'] ) : 5;
    $post_type = isset( $attributes['postType'] ) ? sanitize_text_field( $attributes['postType'] ) : 'post';

    $args = array(
        'post_type' => $post_type,
        'posts_per_page' => $number_of_posts,
        'post_status' => 'publish',
        'orderby' => 'date',
        'order' => 'DESC'
    );

    $query = new WP_Query( $args );

    if ( ! $query->have_posts() ) {
        return '<p>' . esc_html__( 'No posts found.', 'dprt' ) . '</p>';
    }

    $output = '<div class="wp-block-dprt-recent-posts">';
    $output .= '<ul>';

    while ( $query->have_posts() ) {
        $query->the_post();
        $output .= '<li>';
        $output .= '<a href="' . esc_url( get_permalink() ) . '">';
        $output .= esc_html( get_the_title() );
        $output .= '</a>';
        $output .= '<span class="post-date"> - ' . esc_html( get_the_date() ) . '</span>';
        $output .= '</li>';
    }

    $output .= '</ul>';
    $output .= '</div>';

    wp_reset_postdata();

    return $output;
}

Always sanitize attributes and escape output to prevent security vulnerabilities.

Editor Controls and Preview

Create InspectorControls for block settings:

import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import { PanelBody, RangeControl, SelectControl } from "@wordpress/components";
import ServerSideRender from "@wordpress/server-side-render";

export default function Edit({ attributes, setAttributes }) {
    const { numberOfPosts, postType } = attributes;

    return (
        <>
            <InspectorControls>
                <PanelBody title="Settings">
                    <RangeControl
                        label="Number of posts"
                        value={numberOfPosts}
                        onChange={(value) => setAttributes({ numberOfPosts: value })}
                        min={1}
                        max={20}
                    />
                    <SelectControl
                        label="Post Type"
                        value={postType}
                        options={[
                            { label: "Posts", value: "post" },
                            { label: "Pages", value: "page" }
                        ]}
                        onChange={(value) => setAttributes({ postType: value })}
                    />
                </PanelBody>
            </InspectorControls>
            <div {...useBlockProps()}>
                <ServerSideRender block="dprt/recent-posts" attributes={attributes} />
            </div>
        </>
    );
}

ServerSideRender displays a live preview in the editor by calling your render callback via AJAX.

Performance Optimization with Caching

Dynamic blocks can impact performance. Implement caching for expensive operations:

function dprt_render_recent_posts_block( $attributes, $content, $block ) {
    $number_of_posts = isset( $attributes['numberOfPosts'] ) ? absint( $attributes['numberOfPosts'] ) : 5;

    // Create unique cache key based on attributes
    $cache_key = 'dprt_recent_posts_' . md5( serialize( $attributes ) );

    // Try to get cached result
    $cached = get_transient( $cache_key );
    if ( false !== $cached ) {
        return $cached;
    }

    // Generate output (expensive operation)
    $output = dprt_generate_recent_posts_html( $number_of_posts );

    // Cache for 1 hour
    set_transient( $cache_key, $output, HOUR_IN_SECONDS );

    return $output;
}

Clear the cache when content updates:

function dprt_clear_recent_posts_cache( $post_id ) {
    // Clear all recent posts caches
    global $wpdb;
    $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_dprt_recent_posts_%'" );
}
add_action( 'save_post', 'dprt_clear_recent_posts_cache' );

User-Specific Content

Display personalized content based on the logged-in user:

function dprt_render_user_dashboard_block( $attributes, $content, $block ) {
    if ( ! is_user_logged_in() ) {
        return '<p>' . esc_html__( 'Please log in to view your dashboard.', 'dprt' ) . '</p>';
    }

    $current_user = wp_get_current_user();
    $user_posts = count_user_posts( $current_user->ID );

    $output = '<div class="user-dashboard">';
    $output .= '<h3>' . sprintf( esc_html__( 'Welcome, %s', 'dprt' ), esc_html( $current_user->display_name ) ) . '</h3>';
    $output .= '<p>' . sprintf( esc_html__( 'You have published %d posts.', 'dprt' ), $user_posts ) . '</p>';
    $output .= '</div>';

    return $output;
}

External API Integration

Fetch and display external data:

function dprt_render_weather_block( $attributes, $content, $block ) {
    $city = isset( $attributes['city'] ) ? sanitize_text_field( $attributes['city'] ) : 'London';
    $cache_key = 'dprt_weather_' . sanitize_title( $city );

    $weather_data = get_transient( $cache_key );

    if ( false === $weather_data ) {
        $api_url = 'https://api.weatherapi.com/v1/current.json?key=YOUR_KEY&q=' . urlencode( $city );
        $response = wp_remote_get( $api_url );

        if ( is_wp_error( $response ) ) {
            return '<p>' . esc_html__( 'Unable to fetch weather data.', 'dprt' ) . '</p>';
        }

        $weather_data = json_decode( wp_remote_retrieve_body( $response ), true );
        set_transient( $cache_key, $weather_data, 30 * MINUTE_IN_SECONDS );
    }

    $temp = isset( $weather_data['current']['temp_c'] ) ? $weather_data['current']['temp_c'] : 'N/A';

    return '<div class="weather-block">' .
           '<p>' . esc_html( $city ) . ': ' . esc_html( $temp ) . '°C</p>' .
           '</div>';
}

Error Handling

Implement robust error handling:

function dprt_render_dynamic_block( $attributes, $content, $block ) {
    try {
        // Validate attributes
        if ( ! isset( $attributes['requiredField'] ) ) {
            throw new Exception( 'Required field missing' );
        }

        // Attempt operation
        $result = dprt_perform_operation( $attributes );

        if ( false === $result ) {
            throw new Exception( 'Operation failed' );
        }

        return $result;
    } catch ( Exception $e ) {
        // Log error
        error_log( 'Dynamic block error: ' . $e->getMessage() );

        // Return user-friendly message
        return '<p class="block-error">' . esc_html__( 'Content temporarily unavailable.', 'dprt' ) . '</p>';
    }
}

Conclusion

Dynamic blocks combine Gutenberg’s user-friendly interface with PHP’s server-side power. Use them for real-time data, personalization, and database queries. Implement caching for performance, validate all inputs, escape all outputs, and handle errors gracefully. Start with simple dynamic blocks and gradually build more complex functionality as you master the concepts.

  1. Dynamic Blocks Documentation
  2. register_block_type() Reference
  3. ServerSideRender Component
  4. Block Metadata (block.json)
  5. WordPress Query Functions

Call to Action

Streamline your workflow! Block Editor Navigator Pro provides instant block navigation, search, and organization. Find any block in seconds—try it free!