Skip to content

Render Array Patterns

When to Use

Common scenarios have established patterns -- use these proven approaches rather than reinventing solutions.

Pattern: Progressive Enhancement

Build base HTML, enhance with JavaScript via #attached:

$build['interactive_map'] = [
  '#theme' => 'mymodule_map_fallback',
  '#locations' => $locations,
  '#attributes' => ['class' => ['map-container'], 'id' => 'map'],
  '#attached' => [
    'library' => ['mymodule/interactive-map'],
    'drupalSettings' => [
      'myModule' => [
        'mapId' => 'map',
        'locations' => $locations,
      ],
    ],
  ],
];

Benefits:

  • Works without JavaScript (graceful degradation)
  • JavaScript enhances existing HTML
  • Content accessible to crawlers/screen readers

Pattern: Conditional Access

$node = Node::load($nid);
$access_result = $node->access('view', NULL, TRUE);

$build['content'] = [
  '#access' => $access_result,
  'title' => ['#markup' => '<h2>' . $node->label() . '</h2>'],
  'body' => $node->get('body')->view('full'),
];

// Bubble access result's cache metadata
\Drupal::service('renderer')->addCacheableDependency($build, $access_result);

Why capture access result as object:

  • Contains cache metadata (contexts, tags)
  • Prevents caching the wrong access decision for different users

Pattern: Weighted Ordering

$build = [];

$build['header'] = [
  '#markup' => '<h1>Page Title</h1>',
  '#weight' => -100,  // Render first
];

$build['sidebar'] = [
  '#type' => 'container',
  '#weight' => 10,  // Render last
];

$build['content'] = [
  '#markup' => '<p>Main content</p>',
  '#weight' => 0,  // Default, middle
];

// Renders in order: header, content, sidebar

Weight ranges:

  • -100 to -10: Headers, critical content
  • -9 to 9: Default content
  • 10 to 100: Footers, supplementary content

Pattern: Render Array from Entity View

$node = Node::load($nid);

// Full render array for entity
$view_builder = \Drupal::entityTypeManager()->getViewBuilder('node');
$build = $view_builder->view($node, 'teaser');

// Contains all fields, proper cache metadata, theme hooks

View modes: full, teaser, rss, custom modes defined in entity view display config


Pattern: Item List

$build['list'] = [
  '#theme' => 'item_list',
  '#items' => [
    ['#markup' => 'Item 1'],
    ['#markup' => 'Item 2'],
    [
      '#type' => 'link',
      '#title' => t('Item 3 is a link'),
      '#url' => Url::fromRoute('mymodule.page'),
    ],
  ],
  '#list_type' => 'ul',  // 'ul', 'ol', or custom
  '#title' => t('My List'),
  '#attributes' => ['class' => ['custom-list']],
  '#wrapper_attributes' => ['class' => ['list-wrapper']],
];

Pattern: Render Cache Keys from Multiple Variables

$build['complex_content'] = [
  '#markup' => $this->generateContent($user, $config),
  '#cache' => [
    'keys' => [
      'mymodule',
      'complex_content',
      $user->id(),
      $config->id(),
    ],
    'contexts' => ['user', 'languages:language_interface'],
    'tags' => array_merge(
      $user->getCacheTags(),
      $config->getCacheTags()
    ),
  ],
];

Cache keys convention:

  • ['module_name', 'component_name', ...identifiers]
  • Identifiers make the cache unique for this specific render

Pattern: Render Array in AJAX Response

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;

public function ajaxCallback(array &$form, FormStateInterface $form_state) {
  $build = [
    '#theme' => 'mymodule_result',
    '#data' => $this->fetchData(),
  ];

  $response = new AjaxResponse();
  $response->addCommand(new ReplaceCommand('#result-container', $build));

  return $response;
}

AJAX commands automatically render the array before sending to client.


Pattern: Inline Attachments (Small CSS/JS)

$build['styled_box'] = [
  '#markup' => '<div class="special-box">Content</div>',
  '#attached' => [
    'library' => ['mymodule/special-box'],
    'drupalSettings' => ['myModule' => ['mode' => 'fancy']],
    // Inline CSS (use sparingly)
    'html_head' => [[
      [
        '#type' => 'html_tag',
        '#tag' => 'style',
        '#value' => '.special-box { border: 2px solid red; }',
      ],
      'special_box_inline_style',
    ]],
  ],
];

Best practice: Use libraries instead of inline styles; inline only for dynamic/computed values


Pattern: Debugging Render Arrays

// In development
$build['debug'] = [
  '#type' => 'details',
  '#title' => 'Debug: Render Array',
  '#open' => TRUE,
  'content' => [
    '#markup' => '<pre>' . print_r($build, TRUE) . '</pre>',
  ],
];

// Or use Kint (if devel module installed)
kint($build);

Common Mistakes

  • Not reusing patterns -- Solving already-solved problems inefficiently
  • Mixing patterns inconsistently -- Makes codebase hard to understand
  • Over-engineering simple cases -- Sometimes #markup is enough

See Also