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:
x-ref: An Alpine directive that gives you a named "handle" to a DOM element.Rizzy.$data(): A global helper function provided by RizzyUI. It takes a DOM element as an argument and returns the Alpine component instance (x-dataobject) 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.
@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>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.
| Pattern | Use When… | Example |
|---|---|---|
| Orchestration | One component must control another’s action | Open a modal, trigger a toast |
| Global Store | Multiple components need to share state | Dark mode toggle, cart count |
| Events | You need loosely coupled signals | Analytics 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, preferPropsfor 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.
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.
// 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.