Docs Advanced Patterns

Advanced Patterns: Repeater Blocks (Crash-Proof)

Advanced Patterns: Repeater Blocks (Crash-Proof)









2 min read

Repeating items, logo grids, FAQ lists, feature sets – are among the most common causes of editor crashes. Follow these patterns exactly to prevent them.


The Three Crash Rules for Repeaters

Rule 1: Always guard array attributes with Array.isArray(). If a block is inserted for the first time and the attribute has never been set, attributes.logoItems may be undefined. Calling .map() on undefined crashes React immediately.

// ❌ Crashes if logoItems is undefined
attributes.logoItems.map( function(item) {...} )

// ✅ Safe
var logoItems = Array.isArray( attr.logoItems ) ? attr.logoItems : [];
logoItems.map( function(item, idx) {...} )

Rule 2: Extract repeater item panels to named functions. Building a repeater panel inline inside the render loop creates closure bugs where the wrong idx is captured. Extract to a named function.

Rule 3: Never pass dynamic strings into (). () is for i18n and requires a string literal. Calling ( 'Image ' + (idx+1) ) causes the i18n extractor to fail. Build the string outside and pass a literal to ().

// ❌ Wrong — dynamic string in __()
var label = __( 'Remove Image ' + (idx + 1) );

// ✅ Correct — static string in __(), concatenate outside
var label = __( 'Remove', 'wptruss' ) + ' ' + (idx + 1);
// Or simply:
var label = 'Remove ' + (idx + 1);

Complete Crash-Proof Repeater Pattern

( function() {
  'use strict';

  var el         = wp.element.createElement;
  var Fragment   = wp.element.Fragment;
  var PanelBody  = wp.components.PanelBody;
  var TextControl= wp.components.TextControl;
  var Button     = wp.components.Button;
  var MediaUpload= wp.blockEditor.MediaUpload;

  // ── Named function for each repeater item panel ──────────────────────
  function buildLogoPanel( logoItems, idx, setAttr ) {
    var item       = logoItems[idx];
    var panelTitle = 'Logo ' + (idx + 1) + ( item.mediaAlt ? ' — ' + item.mediaAlt : '' );

    return el( PanelBody, { key: idx, title: panelTitle, initialOpen: false },

      // Media upload
      el( MediaUpload, {
        onSelect: function(media) {
          var updated = logoItems.map( function(it, i) {
            return i === idx ? Object.assign( {}, it, { mediaUrl: media.url, mediaAlt: media.alt || '' } ) : it;
          });
          setAttr({ logoItems: updated });
        },
        allowedTypes: ['image'],
        render: function(obj) {
          return el( Button, { onClick: obj.open, variant: 'secondary', isSmall: true },
            item.mediaUrl ? 'Change Image' : 'Upload Image'
          );
        }
      }),

      el( TextControl, {
        label: 'Link URL',
        value: item.linkUrl || '',
        onChange: function(v) {
          var updated = logoItems.map( function(it, i) {
            return i === idx ? Object.assign( {}, it, { linkUrl: v } ) : it;
          });
          setAttr({ logoItems: updated });
        }
      }),

      // Remove button — static string in __()
      el( Button, {
        variant: 'tertiary', isDestructive: true, isSmall: true,
        onClick: function() {
          setAttr({ logoItems: logoItems.filter( function(_, i) { return i !== idx; } ) });
        }
      }, 'Remove this logo' )  // static string — no __() needed for internal tooling
    );
  }

  // ── In the edit function ─────────────────────────────────────────────
  // var logoItems = Array.isArray( attr.logoItems ) ? attr.logoItems : [];
  //
  // el( Fragment, null,
  //   logoItems.map( function(item, idx) {
  //     return buildLogoPanel( logoItems, idx, setAttr );
  //   })
  // ),
  //
  // el( Button, {
  //   variant: 'secondary',
  //   onClick: function() {
  //     setAttr({ logoItems: logoItems.concat([{ mediaUrl: '', mediaAlt: '', linkUrl: '' }]) });
  //   }
  // }, '+ Add Logo' )

}() );