WordPress Gutenberg dynamic block development workspace showing React source code, attribute inspectors, and remote REST API telemetry integration.
WordPress

Building Dynamic Gutenberg Blocks Consuming External REST APIs: React, SSR & Caching

Master developing custom WordPress block editor (Gutenberg) blocks using React, block.json v3, server-side rendering (SSR), and Transients API caching.

The WordPress block editor (Gutenberg) revolutionized web content authoring, replacing the legacy TinyMCE text area and bloated, shortcode-ridden page builders with modern modular architecture. Built entirely on React, Gutenberg allows editorial teams to compose complex digital experiences through declarative component interfaces and modular semantic structures.

However, when an organization needs to display real-time external data within articles—such as live currency exchange rates, financial ticker feeds, ERP warehouse inventory levels, or official bank holiday schedules—developers frequently make a catastrophic architectural error: persisting volatile external API responses statically inside the WordPress database.

If a custom block serializes the raw JSON response of a remote API directly into the rendered HTML of post_content, that data is permanently frozen in time. To ensure content stays continuously synchronized whenever remote sources update, software engineers must build Dynamic Blocks with Server-Side Rendering (SSR).

In this deep-dive tutorial for WordPress software engineers and front-end architects, we will build a dynamic Gutenberg block from scratch using @wordpress/scripts, explore React’s lifecycle inside the block editor (edit.js), implement PHP server-side rendering (render.php), and shield site performance using the WordPress Transients API.


1. Static Blocks vs. Dynamic Blocks: The Architectural Fork

Gutenberg provides two distinct persistence and rendering models for custom blocks:

Architectural MetricStatic BlockDynamic Block (SSR)
Persistence MechanismSerializes final rendered HTML markup directly into post_content in wp_posts.Persists only block attributes within a JSON boundary comment (<!-- wp:my-plugin/live-rates {"currency":"USD"} /-->).
Frontend RenderingServed directly from MySQL as static HTML with zero PHP execution at runtime.WordPress executes a server-side PHP renderer (render.php or render_callback) per request.
Primary Use CasesEditorial typography, static hero banners, feature grids, testimonial cards.External REST API consumers, dynamic product grids, live financial rates, personalized carts.
Markup Invalidation RiskAltering HTML in save() triggers the dreaded “This block contains unexpected or invalid content” error.Zero markup invalidation risk: save() returns null, and HTML compiles dynamically on the server.

💡 Golden Rule: If a block relies on external data feeds that mutate without manual editor intervention in the WordPress dashboard, the block MUST be dynamic. The React save component must return null.


2. Setting Up the Development Toolchain with @wordpress/scripts

The official standard for modern block engineering is @wordpress/scripts, which encapsulates Webpack, Babel, PostCSS, and ESLint without requiring manual configuration:

# Scaffold the block project inside wp-content/plugins/
npx @wordpress/create-block doneapi-live-rates \
  --template @wordpress/create-block/template-esnext \
  --no-plugin

Resulting Plugin File Tree:

doneapi-live-rates/
├── block.json          # Block metadata schema (Version 3)
├── src/
│   ├── edit.js         # React component executed inside the Block Editor
│   ├── index.js        # Client-side registration entrypoint
│   ├── render.php      # Server-side PHP dynamic rendering template
│   ├── style.scss      # Shared styles across frontend and backend
│   └── editor.scss     # Admin-only block editor interface styles
├── build/              # Minified production assets compiled by Webpack
└── doneapi-plugin.php  # Main PHP bootstrap plugin file

3. Defining the Block Manifest: block.json (Schema v3)

The block.json file serves as the definitive single source of truth for the block. It declares attributes, script handles, and dependencies, enabling WordPress to load assets on-demand:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "doneapi/live-rates",
  "version": "1.0.0",
  "title": "DoneAPI Live Exchange Rates",
  "category": "widgets",
  "icon": "chart-line",
  "description": "Displays live foreign exchange rates fetched from DoneAPI microservices.",
  "attributes": {
    "baseCurrency": {
      "type": "string",
      "default": "USD"
    },
    "targetCurrency": {
      "type": "string",
      "default": "COP"
    }
  },
  "supports": {
    "html": false,
    "align": ["wide", "full"]
  },
  "textdomain": "doneapi-rates",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php"
}

4. Crafting the React Editor Experience: edit.js

In edit.js, we construct the interactive visual interface displayed to editors inside Gutenberg, complete with sidebar controls in the Block Inspector:

import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl, Spinner } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

export default function Edit({ attributes, setAttributes }) {
  const { baseCurrency, targetCurrency } = attributes;
  const blockProps = useBlockProps({ className: 'doneapi-rates-preview-box' });

  const [ratePreview, setRatePreview] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    let isMounted = true;
    setIsLoading(true);

    // Fetch live rate for editor preview
    fetch(`https://api.doneapi.com/v1/rates?base=${baseCurrency}&target=${targetCurrency}`)
      .then((res) => res.json())
      .then((data) => {
        if (isMounted) {
          setRatePreview(data.rate);
          setIsLoading(false);
        }
      })
      .catch(() => {
        if (isMounted) setIsLoading(false);
      });

    return () => {
      isMounted = false;
    };
  }, [baseCurrency, targetCurrency]);

  return (
    <>
      <InspectorControls>
        <PanelBody title={__('Exchange Settings', 'doneapi-rates')} initialOpen={true}>
          <SelectControl
            label={__('Base Currency', 'doneapi-rates')}
            value={baseCurrency}
            options={[
              { label: 'USD (US Dollar)', value: 'USD' },
              { label: 'EUR (Euro)', value: 'EUR' },
            ]}
            onChange={(val) => setAttributes({ baseCurrency: val })}
          />
          <SelectControl
            label={__('Target Currency', 'doneapi-rates')}
            value={targetCurrency}
            options={[
              { label: 'COP (Colombian Peso)', value: 'COP' },
              { label: 'MXN (Mexican Peso)', value: 'MXN' },
              { label: 'BRL (Brazilian Real)', value: 'BRL' },
            ]}
            onChange={(val) => setAttributes({ targetCurrency: val })}
          />
        </PanelBody>
      </InspectorControls>

      <div {...blockProps}>
        <div className="doneapi-rates-header">
          <h4>{__('DoneAPI Financial Telemetry [Live Preview]', 'doneapi-rates')}</h4>
        </div>
        <div className="doneapi-rates-body">
          {isLoading ? (
            <Spinner />
          ) : (
            <p>
              1 {baseCurrency} = <strong>${ratePreview?.toLocaleString() || '---'} {targetCurrency}</strong>
            </p>
          )}
        </div>
        <small className="doneapi-rates-note">
          {__('Rendered dynamically via server-side PHP on the frontend.', 'doneapi-rates')}
        </small>
      </div>
    </>
  );
}

And in src/index.js, we register the block with a stateless save routine:

import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import metadata from './block.json';

registerBlockType(metadata.name, {
  edit: Edit,
  save: () => null, // Dynamic blocks render strictly server-side
});

5. Server-Side Rendering (SSR) & Transients Caching: render.php

When a site visitor requests a post containing our dynamic block, WordPress executes render.php. If 10,000 visitors arrive concurrently, executing 10,000 outbound HTTP requests to the third-party API will exhaust server workers and trigger strict API rate limit bans.

The correct architectural solution is caching remote payloads in memory using the WordPress Transients API:

<?php
/**
 * Server-Side Render Template
 *
 * @var array    $attributes Block attributes.
 * @var string   $content    Block inner content.
 * @var WP_Block $block      Block instance.
 */

$base_currency   = sanitize_text_field($attributes['baseCurrency'] ?? 'USD');
$target_currency = sanitize_text_field($attributes['targetCurrency'] ?? 'COP');

$cache_key = 'doneapi_rate_' . md5($base_currency . '_' . $target_currency);
$cached_data = get_transient($cache_key);

if ($cached_data === false) {
    // Cache miss: execute defensive remote HTTP fetch
    $api_url  = add_query_arg([
        'base'   => $base_currency,
        'target' => $target_currency,
    ], 'https://api.doneapi.com/v1/rates');

    $response = wp_remote_get($api_url, [
        'timeout' => 3, // Prevent slow third-party nodes from locking PHP-FPM
        'headers' => [
            'Accept' => 'application/json',
        ],
    ]);

    if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
        $body = json_decode(wp_remote_retrieve_body($response), true);
        if (isset($body['rate'])) {
            $cached_data = [
                'rate'      => (float) $body['rate'],
                'timestamp' => current_time('mysql'),
            ];
            // Store transient in Redis/DB for 15 minutes (900 seconds)
            set_transient($cache_key, $cached_data, 900);
        }
    }
}

$wrapper_attributes = get_block_wrapper_attributes(['class' => 'doneapi-rates-widget']);
?>

<div <?php echo $wrapper_attributes; ?>>
    <div class="doneapi-rates-card">
        <header class="doneapi-rates-title">
            <span><?php esc_html_e('Financial Exchange Rate', 'doneapi-rates'); ?></span>
        </header>
        <?php if (!empty($cached_data)) : ?>
            <div class="doneapi-rates-content">
                <div class="doneapi-rate-display">
                    <span class="doneapi-currency-code"><?php echo esc_html($base_currency . ' / ' . $target_currency); ?></span>
                    <span class="doneapi-rate-number">$<?php echo number_format($cached_data['rate'], 2); ?></span>
                </div>
                <footer class="doneapi-rates-footer">
                    <small><?php echo esc_html__('Updated:', 'doneapi-rates') . ' ' . esc_html($cached_data['timestamp']); ?></small>
                </footer>
            </div>
        <?php else : ?>
            <div class="doneapi-rates-fallback">
                <p><?php esc_html_e('Exchange rate telemetry is temporarily unavailable.', 'doneapi-rates'); ?></p>
            </div>
        <?php endif; ?>
    </div>
</div>

6. Registering the Block in the Core Plugin

To register the block with automatic asset resolution and translation support, register the build directory in your main plugin file:

<?php
/**
 * Plugin Name: DoneAPI Gutenberg Blocks
 * Description: Enterprise dynamic block suite connected to cloud microservices.
 * Version: 1.0.0
 * Author: DoneAPI Engineering Team
 */

if (!defined('ABSPATH')) {
    exit;
}

add_action('init', function () {
    // Automatically registers block.json metadata, scripts, styles, and render.php
    register_block_type(__DIR__ . '/build');
});

7. WordPress Engineering & API Consulting with DoneAPI

Building bespoke Gutenberg blocks and high-scale WordPress architectures requires mastery of modern JavaScript (React, JSX, Redux state stores) paired with rock-solid PHP backend engineering (transient caching, concurrency guards, and sub-millisecond query execution).

At DoneAPI, we help digital media publishers, financial platforms, and e-commerce leaders across the Americas:

  • Build Advanced Gutenberg Block Libraries: Designing bespoke block systems tailored strictly to your corporate design tokens.
  • Microservices & API Integrations: Connecting WordPress with ERPs (SAP, NetSuite), CRMs (Salesforce, HubSpot), and real-time billing gateways.
  • Speed & Core Web Vitals Audits: Optimizing high-traffic WordPress websites to maximize performance scores across LCP, INP, and CLS.
  • Turnkey Production Plugins: Deploy battle-tested solutions like our VikBooking + Mercado Pago ($7 USD) gateway for hospitality reservation engines.

💬 Need to construct dynamic Gutenberg blocks or integrate WordPress with external microservices at scale? Connect directly with our senior software engineers on WhatsApp for technical advisory.

Build Dynamic Gutenberg Blocks & Custom Plugins with DoneAPI

Harness the full power of React inside WordPress without sacrificing backend server speed or stability.

Speak with a Senior WordPress Engineer on WhatsApp

8. Conclusion

The Gutenberg block editor is far more than an editorial canvas: it is a full-fledged client-side React runtime natively coupled to the WordPress backend.

Mastering dynamic blocks with server-side rendering and Transients API caching empowers your engineering team to enrich digital publications with real-time REST API data while guaranteeing sub-second page loads and shielding upstream services from unexpected traffic surges.

Herramientas de Inteligencia Artificial para emprendedores

Desbloquea tu arsenal de automatización.

Regístrate gratis y accede a plantillas para n8n y Make.com, packs de prompts probados para IA, y guías exclusivas diseñadas para escalar tu negocio digital.

Crear cuenta y obtén recursos gratis