Underrated Alpine API Features

Tons of attention for new Alpiners is focused on Directives and Magics, but the Alpine Global is packed full of interesting options that can be very useful.

Everyones going on an on about @click, x-text, and $store, but what about the Alpine globals!? Let’s dive into some lesser known, but quite useful — dare I say powerful? — features of the Alpine API.

I maintain the @types/alpinejs package. I’ve worked hard to properly Type and even document the Alpine API. Even if you’re not using TypeScript, installing this package can give you helpful explanations of the API as you use these methods.

Alpine​.reactive

interface Alpine {
  reactive<T>(obj: T): T;
}

Underneath Alpine is the powerful @vue/reactivity package. When you make new components, or stores, or add values to existing components, Alpine wraps them in reactive proxies. You’ve probably seen this when trying to log an object from a component.

Well, you can use Alpine.reactive to make your own reactive objects! This is useful mainly for plugins that might want to hook into reactivity without waiting for values to exist in a component.

import Alpine from 'alpinejs';

const data = Alpine.reactive({ count: 0 });
Alpine.effect(() => {
  console.log(data.count);
});
data.count++;

Alpine.effect is also a way to register a reactive effect, like x-effect does.

Bonus: Alpine​.raw

interface Alpine {
  raw<T>(obj: T): T;
}

To clean up those Proxies in those logs, Alpine.raw does the opposite of reactive! It lets you simply access the internal object without concern for proxy wrapper. Can really make console debugging a lot easier.

<button
  type="button"
  @click="
    console.log(Alpine.raw(cartItems))
  "></button>

Alpine​.addRootSelector

interface Alpine {
  addRootSelector(selectorCallback: () => string): void;
}

Ever make a fancy clean Headless Alpine component (like in @alpinejs/ui) but not enjoy the process of adding the x-data attribute to the elements, just to make them work? addRootSelector is here to help!

It basically adds new selectors for Alpine to initialize as if they are x-data, indicating the root of a new Alpine component. Just pass in a function that returns a css selector string.

import Alpine from 'alpinejs';

Alpine.addRootSelector(() => `[${Alpine.prefixed('dialog')}]`);
<dialog x-dialog>Alpine sure is cool!</dialog>

Alpine.prefixed is a helper to transform the attribute name to respect Alpine.prefix configuration.

Bonus: Alpine​.addInitSelector

interface Alpine {
  addInitSelector(selectorCallback: () => string): void;
}

Like addRootSelector, but for elements that you only want to evaluate as standalone actions. This is like x-init! x-init can be used without being inside an x-data to help run simple startup expressions.

Alpine​.addScopeToNode

interface Alpine {
  addScopeToNode(
    node: Element,
    data: Record<string, unknown>,
    referenceNode?: Element,
  ): () => void;
}

addScopeToNode is the JavaScript API equivalent to x-data! It allows your code to add new data to an element, for directives or children to access.

The optionally passed in referenceNode allows you to pass in a different node for the element to “inherit” from. This is useful for things like x-teleport where the element is somewhere else, but reactively interacts with a different component tree!

import Alpine from 'alpinejs';

const el = document.querySelector('[x-data]');
const newPage = document.createElement('div');

Alpine.addScopeToNode(newPage, { page: 'home' }, el);

document.body.append(newPage);

Note: addScopeToNode returns a function that can be called to remove the scope from the element. This is useful for cleanup in Single Page Applications.

Bonus: Alpine​.$data

interface Alpine {
  $data(node: Element): void;
}

This gets the data context of the element! It’s the same way expressions access the data context. You get a flattened object of the entire data stack that expressions on that element can access.

import Alpine from 'alpinejs';

const el = document.querySelector('button');

el.addEventListener('click', () => {
  if ('cart' in Alpine.$data(el)) console.log('Cart exists!');
  else console.log('No cart here!');
});

Honourable Mentions

Alpine​.throttle

interface Alpine {
  throttle(callback: Function, wait: number): Function;
}

Sometimes you need to throttle a function, but you aren’t using @click.throttle. Well, you can use the same underlying functionality with Alpine.throttle! Now you can have your functions run only once every so often.

import Alpine from 'alpinejs';

const throttledLog = Alpine.throttle(
  (msg) => console.log('Message:', msg),
  1000,
);

throttledLog('Hello'); // logs
throttledLog('World'); // doesn't log

It’s also FULLY type safe! So the type of function that goes in is the type of function that comes out!

Alpine​.debounce

interface Alpine {
  debounce(callback: Function, wait: number): Function;
}

Just like the above, for debounce! Now you can have your functions run only after a certain amount of time has passed since the last call, always with just the last arguments passed in.

import Alpine from 'alpinejs';

const debouncedLog = Alpine.debounce(
  (msg) => console.log('Latest Message:', msg),
  1000,
);

debouncedLog('Hello'); // doesn't log
debouncedLog('World'); // logs.... later

Oh, and it’s type safe too!

Wrap Up

Theres a lot of amazing power in the Alpine API! While these aren’t all things you should be diving into regularly, they can be very useful for providing clean interfaces to highly reusable components. Let me know if there’s any you think are even better!