Skip to main content

Repeater and Flexible Content Render Loops

Loop-heavy templates remain stable only when row traversal, layout dispatching, and empty-state guards are designed intentionally.

Learning Focus

You will implement robust Repeater and Flexible Content loop patterns, isolate layouts into dedicated renderers, and validate row/layout payloads with WP-CLI to prevent runtime regressions.

Concept Overview

Repeater loops are deterministic: each row has the same subfield shape. Flexible Content loops are polymorphic: each row can represent a different layout and payload shape. That difference is the root of most template complexity in ACF Pro projects.

A scalable pattern keeps loops thin and delegates markup to focused functions or partials. The loop does selection and guard logic; the renderer handles output. This separation improves testability, reduces merge conflicts, and keeps unknown layouts visible via logging.

Without these boundaries, teams often accumulate giant template files where every layout branch manipulates data and markup inline. Small schema changes then trigger broad regressions because no one can confidently reason about loop behavior under partial or malformed payloads.

Core Idea

Treat loops as dispatch engines: validate row/layout state, route to dedicated renderers, and always handle unknown or empty data paths explicitly.

Why It Matters

ApproachWhat HappensImpact in Production
Guard have_rows() and row payloads before renderingEmpty and malformed rows are handled predictablyCleaner output and fewer warnings
Dispatch Flexible layouts to dedicated renderersLayout logic is isolated and maintainableFaster updates and safer releases
Include unknown-layout default branch with logsSchema drift is visible immediatelyReduced debugging time during incidents
Keep row-processing limits for heavy pagesPrevents excessive rendering workBetter performance under large editorial payloads
Inline everything in one template (wrong pattern)High cognitive load and fragile editsFrequent regressions and slower delivery

Reference Table

Term/APISignature/SyntaxPurposeKey Notes
have_rows()`have_rows(string $selector, intstring $post_id = false): bool`Check row availability and control loop lifecycle
the_row()the_row(): voidAdvance current row pointerCall inside while (have_rows(...))
get_sub_field()get_sub_field(string $selector, bool $format_value = true): mixedRead subfield from current rowEscape and guard by context
get_row_layout()`get_row_layout(): stringfalse`Resolve current Flexible layout slug
Repeater config'type' => 'repeater', 'sub_fields' => [...]Define fixed-shape row collectionsUse for FAQ lists, stats rows, link sets
Flexible config'type' => 'flexible_content', 'layouts' => [...]Define variable layout rowsUse when row types differ significantly
wp eval payload auditwp eval '$rows=get_field("field",<id>); ...'Verify row count and layout mapAdd to release smoke checks
error_log() in default brancherror_log('[acf-flex] unknown layout=' . $layout)Detect unexpected layouts in productionCritical for schema drift observability

Practical Use Cases

Use Case 1 — Render FAQ accordion from Repeater rows with robust guards

A support team updates FAQ content weekly. You need an editor-friendly Repeater structure and a stable frontend accordion component that ignores incomplete rows.

  1. Register FAQ Repeater field in code.
  2. Add question/answer subfields with required flags.
  3. Render rows only when both values are present.
  4. Use safe output escaping for summary/body contexts.
  5. Validate row payload and count from CLI.
wp-content/themes/clinic-pro/inc/acf/faq-loop-model.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_faq_loop_model',
'title' => 'FAQ Loop Model',
'fields' => [
[
'key' => 'field_faq_items',
'label' => 'FAQ Items',
'name' => 'faq_items',
'type' => 'repeater',
'layout' => 'row',
'button_label' => 'Add FAQ Row',
'sub_fields' => [
[
'key' => 'field_faq_question',
'label' => 'Question',
'name' => 'faq_question',
'type' => 'text',
'required' => 1,
],
[
'key' => 'field_faq_answer',
'label' => 'Answer',
'name' => 'faq_answer',
'type' => 'textarea',
'required' => 1,
],
],
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
]]],
]);
});

add_action('template_redirect', function (): void {
if (!is_page()) {
return;
}

if (!have_rows('faq_items')) {
return;
}

echo '<section class="faq-list" aria-label="Frequently asked questions">';

while (have_rows('faq_items')) {
the_row();

$question = (string) get_sub_field('faq_question');
$answer = (string) get_sub_field('faq_answer');

if ($question === '' || $answer === '') {
continue;
}

echo '<details class="faq-item">';
echo '<summary>' . esc_html($question) . '</summary>';
echo '<div>' . wp_kses_post($answer) . '</div>';
echo '</details>';
}

echo '</section>';
});
terminal: command
wp eval '$id=1360; update_field("faq_items", [["faq_question"=>"How long is onboarding?","faq_answer"=>"Usually 14 days."],["faq_question"=>"Is support included?","faq_answer"=>"Yes, weekdays."]], $id); $rows=get_field("faq_items",$id); echo count((array)$rows) . PHP_EOL; echo $rows[0]["faq_question"] . PHP_EOL;'
wp post meta get 1360 faq_items
terminal: output
2
How long is onboarding?
a:2:{...}
note

Row-level guards allow editors to draft partial entries without breaking frontend markup.

Use Case 2 — Dispatch Flexible layouts into dedicated renderer map

A landing-page system supports three approved layouts (hero, logos, cta). Marketing can reorder layouts, but engineering wants strict rendering control and observability for unknown slugs.

  1. Register Flexible field with approved layout slugs.
  2. Implement layout renderer functions.
  3. Build a dispatcher map from slug to callable.
  4. Use default logging path for unknown layouts.
  5. Audit layout sequence via CLI before release.
wp-content/themes/clinic-pro/inc/acf/flexible-render-dispatcher.php
<?php

declare(strict_types=1);

add_action('acf/init', function (): void {
acf_add_local_field_group([
'key' => 'group_landing_flexible_loop',
'title' => 'Landing Flexible Loop',
'fields' => [
[
'key' => 'field_page_sections',
'label' => 'Page Sections',
'name' => 'page_sections',
'type' => 'flexible_content',
'button_label' => 'Add Section',
'layouts' => [
[
'key' => 'layout_hero',
'name' => 'hero',
'label' => 'Hero',
'display' => 'block',
'sub_fields' => [
['key' => 'field_hero_title', 'label' => 'Hero Title', 'name' => 'hero_title', 'type' => 'text'],
],
],
[
'key' => 'layout_logos',
'name' => 'logos',
'label' => 'Logos',
'display' => 'block',
'sub_fields' => [
['key' => 'field_logo_ids', 'label' => 'Logo IDs', 'name' => 'logo_ids', 'type' => 'textarea'],
],
],
[
'key' => 'layout_cta',
'name' => 'cta',
'label' => 'CTA',
'display' => 'block',
'sub_fields' => [
['key' => 'field_cta_label', 'label' => 'CTA Label', 'name' => 'cta_label', 'type' => 'text'],
['key' => 'field_cta_url', 'label' => 'CTA URL', 'name' => 'cta_url', 'type' => 'url'],
],
],
],
],
],
'location' => [[[
'param' => 'post_type',
'operator' => '==',
'value' => 'page',
]]],
]);
});

function clinic_render_layout_hero(): void
{
$title = (string) get_sub_field('hero_title');
if ($title !== '') {
echo '<section class="hero"><h2>' . esc_html($title) . '</h2></section>';
}
}

function clinic_render_layout_logos(): void
{
$raw = (string) get_sub_field('logo_ids');
$ids = array_filter(array_map('intval', array_map('trim', explode(',', $raw))));
if (count($ids) === 0) {
return;
}

echo '<section class="logos">';
foreach ($ids as $id) {
echo wp_get_attachment_image($id, 'medium');
}
echo '</section>';
}

function clinic_render_layout_cta(): void
{
$label = (string) get_sub_field('cta_label');
$url = (string) get_sub_field('cta_url');
if ($label !== '' && $url !== '') {
echo '<section class="cta"><a href="' . esc_url($url) . '">' . esc_html($label) . '</a></section>';
}
}

add_action('template_redirect', function (): void {
if (!is_page() || !have_rows('page_sections')) {
return;
}

$dispatch = [
'hero' => 'clinic_render_layout_hero',
'logos' => 'clinic_render_layout_logos',
'cta' => 'clinic_render_layout_cta',
];

while (have_rows('page_sections')) {
the_row();
$layout = (string) get_row_layout();

if (!isset($dispatch[$layout])) {
error_log('[flex-dispatch] unknown layout=' . $layout . ' page=' . get_the_ID());
continue;
}

call_user_func($dispatch[$layout]);
}
});
terminal: command
wp eval '$id=1360; $rows=[["acf_fc_layout"=>"hero","hero_title"=>"ACF Loop Hero"],["acf_fc_layout"=>"cta","cta_label"=>"Book Now","cta_url"=>"/book"]]; update_field("page_sections",$rows,$id); $saved=get_field("page_sections",$id); echo count((array)$saved) . PHP_EOL; foreach((array)$saved as $row){echo $row["acf_fc_layout"] . PHP_EOL;}'
terminal: output
2
hero
cta
ACF Pro Required

Flexible Content render loops and layout dispatching depend on ACF Pro.

Use Case 3 — Edge case: unknown layouts and oversized row payloads

After a content migration, some pages contain an unapproved pricing_grid layout and 60+ section rows. Fragile templates crash or degrade. Robust loops enforce row limits and log unknown layouts.

❌ Fragile Pattern

wp-content/themes/clinic-pro/template-parts/loop-fragile.php
<?php

declare(strict_types=1);

add_action('template_redirect', function (): void {
while (have_rows('page_sections')) {
the_row();
$layout = get_row_layout();
include locate_template('partials/layout-' . $layout . '.php');
}
});

✅ Robust Pattern

wp-content/themes/clinic-pro/template-parts/loop-robust.php
<?php

declare(strict_types=1);

add_action('template_redirect', function (): void {
if (!is_page() || !have_rows('page_sections')) {
return;
}

$allowedLayouts = ['hero', 'logos', 'cta'];
$maxRows = 25;
$rowIndex = 0;

while (have_rows('page_sections')) {
the_row();
$rowIndex++;

if ($rowIndex > $maxRows) {
error_log('[loop-robust] row_limit_exceeded page=' . get_the_ID() . ' rows=' . $rowIndex);
break;
}

$layout = (string) get_row_layout();
if (!in_array($layout, $allowedLayouts, true)) {
error_log('[loop-robust] unknown_layout=' . $layout . ' page=' . get_the_ID());
continue;
}

$templatePath = locate_template('partials/layout-' . $layout . '.php');
if ($templatePath === '') {
error_log('[loop-robust] missing_partial=' . $layout . ' page=' . get_the_ID());
continue;
}

include $templatePath;
}
});
terminal: command
wp eval '$rows=get_field("page_sections",1360); echo "rows=" . count((array)$rows) . PHP_EOL; $allowed=["hero","logos","cta"]; $unknown=[]; foreach((array)$rows as $row){$l=$row["acf_fc_layout"] ?? ""; if($l!=="" && !in_array($l,$allowed,true)){$unknown[]=$l;}} print_r($unknown);'
wp eval '$rows=get_field("page_sections",1360); echo (count((array)$rows) > 25 ? "over_limit" : "within_limit") . PHP_EOL;'
terminal: output
rows=2
Array
(
)
within_limit
warning

Row limits are not arbitrary. They are a practical guardrail against accidental payload explosions during migrations or bulk imports.

Common Mistakes

MistakeRoot CauseWhat Breaks in ProductionCorrect Pattern
Rendering rows without have_rows() guardAssumes rows always existEmpty wrappers, warnings, and noisy logsGuard early and return fast
No default handling for unknown layoutsLayout list treated as static foreverNew layouts silently fail to renderAdd default branch and log unknown slugs
Inline branching and markup in one giant loopNo separation of concernsHard reviews and regression-prone changesUse dispatcher map + dedicated renderers
No row-count limits on heavy pagesUnbounded editor payload growthPerformance degradation and timeout riskEnforce max rows and log overflow
Missing empty checks for subfieldsPartial rows produce broken UIBlank summaries, empty CTAs, malformed sectionsValidate required subfields per row
Skipping CLI loop auditsLoop regressions found too lateProduction troubleshooting overheadAdd payload count/layout checks in release workflow
Deep Dive: Why Flexible Loop Bugs Feel Intermittent

Flexible loop bugs are data-dependent, so only specific pages trigger them. A page with two approved layouts may render perfectly while another with one migrated unknown layout fails. This causes false confidence during spot checks. The fix is to inspect saved layout arrays for all high-traffic pages and compare them to your allowed layout list. Add CLI audits to release criteria so unknown slugs are discovered before users do.

wp eval '$rows=get_field("page_sections",1360); foreach((array)$rows as $i=>$row){echo "row=".($i+1)." layout=".($row["acf_fc_layout"] ?? "missing") . PHP_EOL;}'

Best Practices

  1. Use guard clauses first (is_page, have_rows) to keep loops predictable.
  2. Dispatch Flexible layouts by slug-to-renderer map, not giant inline conditionals.
  3. Define and enforce allowed layout slugs in one shared constant/source.
  4. Log unknown layouts and missing partials explicitly with post IDs.
  5. Validate required subfields before outputting row wrappers.
  6. Set practical row limits for large editorial sections.
  7. Run CLI audits for row counts and layout slugs before each release.

Hands-On Practice

Exercise 1: Register and populate FAQ Repeater rows

Create wp-content/themes/clinic-pro/inc/acf/faq-loop-model.php and run:

wp eval '$id=1360; update_field("faq_items", [["faq_question"=>"Q1","faq_answer"=>"A1"],["faq_question"=>"Q2","faq_answer"=>"A2"]], $id); echo count((array)get_field("faq_items",$id)) . PHP_EOL;'

After completing this exercise, output should be:

2

Exercise 2: Register Flexible sections and store layout sequence

Run:

wp eval '$id=1360; update_field("page_sections", [["acf_fc_layout"=>"hero","hero_title"=>"Loop Hero"],["acf_fc_layout"=>"cta","cta_label"=>"Book","cta_url"=>"/book"]], $id); $rows=get_field("page_sections",$id); echo count((array)$rows) . PHP_EOL;'

After completing this exercise, output should be:

2

Exercise 3: Audit layout slugs against allowed map

Run:

wp eval '$allowed=["hero","logos","cta"]; $rows=get_field("page_sections",1360); $unknown=[]; foreach((array)$rows as $row){$layout=$row["acf_fc_layout"] ?? ""; if($layout!=="" && !in_array($layout,$allowed,true)){$unknown[]=$layout;}} print_r($unknown);'

After completing this exercise, output should be:

Array
(
)

Exercise 4: Simulate unknown layout and verify detection

Run:

wp eval '$id=1360; update_field("page_sections", [["acf_fc_layout"=>"hero","hero_title"=>"Loop Hero"],["acf_fc_layout"=>"pricing_grid"]], $id);'
wp eval '$allowed=["hero","logos","cta"]; $rows=get_field("page_sections",1360); $unknown=[]; foreach((array)$rows as $row){$layout=$row["acf_fc_layout"] ?? ""; if($layout!=="" && !in_array($layout,$allowed,true)){$unknown[]=$layout;}} print_r($unknown);'

After completing this exercise, output should include:

Array
(
[0] => pricing_grid
)

Exercise 5: Restore approved layouts and run row-limit check

Run:

wp eval '$id=1360; update_field("page_sections", [["acf_fc_layout"=>"hero","hero_title"=>"Loop Hero"],["acf_fc_layout"=>"logos","logo_ids"=>"1,2"],["acf_fc_layout"=>"cta","cta_label"=>"Book","cta_url"=>"/book"]], $id); $rows=get_field("page_sections",$id); echo (count((array)$rows) > 25 ? "over_limit" : "within_limit") . PHP_EOL;'

After completing this exercise, output should be:

within_limit

CLI Reference

CommandPurposeReal Example Output
wp eval '$rows=get_field("faq_items",1360); echo count((array)$rows) . PHP_EOL;'Count FAQ Repeater rows2
wp eval '$rows=get_field("faq_items",1360); echo $rows[0]["faq_question"] . PHP_EOL;'Inspect first Repeater rowHow long is onboarding?
wp eval '$rows=get_field("page_sections",1360); foreach((array)$rows as $row){echo $row["acf_fc_layout"] . PHP_EOL;}'Print Flexible layout sequencehero cta
wp eval '$allowed=["hero","logos","cta"]; ... print_r($unknown);'Detect unknown layout slugsArray ( )
wp post meta get 1360 page_sectionsInspect raw Flexible payload storagea:3:{...}
wp eval '$rows=get_field("page_sections",1360); echo count((array)$rows) > 25 ? "over_limit" : "within_limit";'Enforce row-limit policywithin_limit
wp eval '$rows=get_field("page_sections",1360); print_r($rows[0]);'Debug first layout payload shapeArray ( [acf_fc_layout] => hero ... )
`wp plugin list --status=activegrep advanced-custom-fields-pro`Confirm ACF Pro dependency

What's Next

tip

Revisit this lesson whenever editors request new layout variants, because loop architecture quality determines whether those changes are easy or dangerous.