Advanced Patterns: Repeater Blocks (Crash-Proof)
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' )
}() );