RizzyUI

Advanced: Orchestrating Components

So far, we've built self-contained components. But in a real application, components need to work together. A button in a table row might need to open a modal. A search input in the header might need to update a list in the main content area.

This is orchestration: making separate components communicate and control each other. In RizzyUI, this is achieved with a simple and powerful pattern that combines Alpine's x-ref attribute with a special RizzyUI helper, Rizzy.$data().

The Core Tools for Orchestration

To make components talk, you only need two things:

  1. x-ref: An Alpine directive that gives you a named "handle" to a DOM element.
  2. Rizzy.$data(): A global helper function provided by RizzyUI. It takes a DOM element as an argument and returns the Alpine component instance (x-data object) associated with it.

By combining these two, you can get direct access to the methods and properties of any other RizzyUI component on the page.

Orchestration Examples

Example 1: A Delete Confirmation Modal

A button in one component opens and passes data to a separate <RzDialog> component.

ItemManager.razor
@attribute [RzAlpineCodeBehind]

<RzAlpineComponent For="this" Name="itemManager">
  <div class="text-center">
    <p class="text-foreground">Item to delete: <strong x-text="itemToDelete.name || 'None'"></strong></p>
    <RzButton Variant="ThemeVariant.Destructive" x-on:click="confirmDelete({ id: 1, name: 'First Item' })" class="mt-2">
      Delete Item
    </RzButton>
  </div>

  <!-- The Modal to be controlled -->
  <RzDialog x-ref="confirmModal">
    <DialogContent>
        <DialogHeader>
            <DialogTitle>Confirm Deletion</DialogTitle>
        </DialogHeader>
        <div class="p-6">
            <p>Are you sure you want to delete <strong x-text="itemToDelete.name"></strong>?</p>
        </div>
        <DialogFooter>
            <DialogClose AsChild>
                <RzButton>Cancel</RzButton>
            </DialogClose>
            <RzButton Variant="ThemeVariant.Destructive">Confirm Delete</RzButton>
        </DialogFooter>
    </DialogContent>
  </RzDialog>
</RzAlpineComponent>
ItemManager.razor.js
export default () => ({
  itemToDelete: {},
  modal: null,

  init() {
    // It's safer to get the reference inside a $nextTick to ensure the modal has been initialized by Alpine
    this.$nextTick(() => {
      this.modal = Rizzy.$data(this.$refs.confirmModal);
    });
  },

  confirmDelete(item) {
    this.itemToDelete = item;

    // Defensive check: re-acquire the reference in case of HTMX swaps
    const modal = Rizzy.$data(this.$refs.confirmModal);
    if (modal?.openModal) {
      modal.openModal();
    } else {
      // Fallback or error handling if the modal isn't ready yet
      console.warn("Modal instance not available yet. Retrying in a moment.");
      this.$nextTick(() => {
        const modalRetry = Rizzy.$data(this.$refs.confirmModal);
        modalRetry?.openModal();
      });
    }
  }
});

Communication Patterns: When to Use Orchestration

Orchestration is powerful, but it's not the only way for components to communicate. Choosing the right pattern is key to building maintainable applications.

PatternUse When…Example
OrchestrationOne component must control another’s actionOpen a modal, trigger a toast
Global StoreMultiple components need to share stateDark mode toggle, cart count
EventsYou need loosely coupled signalsAnalytics pings, global alerts

Best Practices for Orchestration

  • Use for Imperative Actions: Orchestration is best for *telling* a component to *do something* (e.g., open(), close()). For sharing data, prefer Props for initial data and global stores for ongoing reactive state.
  • Keep APIs Minimal: When designing a controllable component, expose a small, predictable API. This reduces coupling and makes your components easier to reason about.
  • Avoid Circular Orchestration: Avoid patterns where Component A controls Component B, and Component B also controls Component A. This can lead to infinite loops.
  • Re-acquire References After HTMX Swaps: If a target component might be replaced by an HTMX swap, re-acquire the reference just before you use it to avoid stale references.

Advanced Scenarios & Troubleshooting

Lifecycle: The Target Component Isn't Ready

Problem: Rizzy.$data() returns null or undefined inside your init() method.

Cause: You're likely trying to access the component before it has been initialized by Alpine. This is common if the target is rendered conditionally or further down the DOM.

Fix: Wrap your orchestration setup in this.$nextTick(() => { ... }). This ensures your code runs after Alpine has completed its initial DOM updates.

Source
init() {
  this.$nextTick(() => {
    this.sidebar = Rizzy.$data(this.$refs.sidebar);
  });
}

HTMX: Handling Stale References

Problem: Orchestration works once, but fails after an HTMX partial update.

Cause: The original DOM element you referenced was replaced by the HTMX swap, making your stored Alpine instance stale.

Fix: Re-acquire the reference inside the method that performs the action, ensuring you always have the latest instance.

Source
// Good practice for HTMX environments
confirmDelete(item) {
  this.itemToDelete = item;
  // Re-acquire the reference every time
  const modal = Rizzy.$data(this.$refs.confirmModal);
  if (modal?.openModal) {
    modal.openModal();
  }
}

Next Steps

You now have the complete toolkit for building complex, interactive UIs with RizzyUI. The final piece of the puzzle is knowing what to do when things go wrong.

The next page covers essential tips and tools for Debugging and Best Practices.