Modern "Once" Usage in Custom Drupal Module or Theme JavaScripts (Behaviors)

Submitted by admin on

Using once in Drupal 11 Behaviors

In Drupal 9+, the standalone once library replaced jQuery.once. In Drupal 11 it's the standard approach.


Basic Behavior Structure with once

(function (Drupal, once) {
  'use strict';

  Drupal.behaviors.myBehavior = {
    attach(context, settings) {
      // once(id, selector, context) — returns only NEW/unprocessed elements
      once('myBehavior', '.my-selector', context).forEach((element) => {
        // This runs only once per element, even if attach() is called multiple times
        element.addEventListener('click', () => {
          console.log('clicked', element);
        });
      });
    },

    detach(context, settings, trigger) {
      // Clean up when elements are removed (trigger === 'unload')
      if (trigger === 'unload') {
        once.remove('myBehavior', '.my-selector', context).forEach((element) => {
          // remove listeners, reset state, etc.
        });
      }
    }
  };

}(Drupal, once));

Key points:

  • Pass once as an IIFE parameter — don't rely on a global
  • once(id, selector, context) returns an array of elements not yet processed with that ID
  • The id must be unique per behavior to avoid collisions
  • Always pass context — never query the entire document unless intentional

Declaring Dependencies in *.libraries.yml

# mymodule.libraries.yml

my-library:
  version: VERSION
  js:
    js/my-script.js: {}
  dependencies:
    - core/drupal          # Drupal.behaviors, Drupal.t(), etc.
    - core/once            # the once() function
    - core/drupalSettings  # access to drupalSettings object in JS

# If you need jQuery (avoid if possible in Drupal 11):
my-library-with-jquery:
  js:
    js/my-jquery-script.js: {}
  dependencies:
    - core/drupal
    - core/once
    - core/jquery

Common Core Libraries Reference

Dependency

Provides

core/drupal

Drupal, Drupal.behaviors, Drupal.t()

core/once

once() standalone function

core/drupalSettings

drupalSettings JS object

core/jquery

jQuery / $ (use sparingly)

core/ajax

Drupal AJAX API

core/announce

Drupal.announce() for accessibility


Attaching the Library

In a module (hook_page_attachments or render array):

// In a render array
$build['#attached']['library'][] = 'mymodule/my-library';

// With settings passed to JS
$build['#attached']['drupalSettings']['mymodule']['myKey'] = 'myValue';

In a theme (.theme file or hook_page_attachments):

function mytheme_page_attachments_alter(array &$attachments) {
  $attachments['#attached']['library'][] = 'mytheme/my-library';
}

In a Twig template:

{{ attach_library('mymodule/my-library') }}

Accessing drupalSettings in JS

(function (Drupal, once, drupalSettings) {

  Drupal.behaviors.myBehavior = {
    attach(context, settings) {
      // 'settings' param mirrors drupalSettings at attach time
      const myValue = settings.mymodule?.myKey;
      // or directly:
      const myValue2 = drupalSettings.mymodule?.myKey;

      once('myBehavior', '.my-element', context).forEach((el) => {
        el.dataset.value = myValue;
      });
    }
  };

}(Drupal, once, drupalSettings));

Common Mistakes to Avoid

// ❌ Wrong — no context, processes whole document every attach
once('thing', '.item').forEach(...);

// ✅ Correct — scoped to context
once('thing', '.item', context).forEach(...);

// ❌ Wrong — jQuery.once() is removed in Drupal 9+
$('.item').once('thing').each(...);

// ❌ Wrong — relying on global `once` without declaring dependency
// (works by accident but breaks if library order changes)

// ✅ Correct — always pass once as IIFE param and declare core/once
(function(Drupal, once) { ... }(Drupal, once));

The once library marks processed elements with a data-once="id" attribute internally, so even if attach() fires multiple times (AJAX, BigPipe, etc.), your code only runs once per element.

Recent Content
Drupal Topics