Working with State and Reactivity
You've learned how to create a component with x-data and pass initial props. Now, let's explore how to make your components more dynamic. This page introduces three core reactive tools: watching state with $watch, reacting automatically with x-effect, and sharing global state with Alpine.store().
$watch: Responding to State Changes
Sometimes, you need to perform an action whenever a piece of your component's state changes. The $watch magic property is perfect for this. It lets you "watch" a property and run a function whenever its value is updated. Common use cases include validating user input as they type or auto-saving draft content.
Example: A Character Counter
Let's create a textarea with a character counter that updates in real-time. Note that since RzInputTextArea is a form component, it must be placed within an EditForm.
@attribute [RzAlpineCodeBehind]
@using RizzyUI.Docs.Models
<EditForm Model="@_model">
<RzAlpineComponent For="this" Name="charCounter" AsChild>
<div class="w-full">
<RzInputTextArea For="@(() => _model.Message)" class="w-full min-h-24" x-model="message" />
<p class="text-sm text-muted-foreground mt-2 text-right">
Character count: <span x-text="count"></span>
</p>
</div>
</RzAlpineComponent>
</EditForm>
@code {
private class CharacterCounterModel
{
public string Message { get; set; } = string.Empty;
}
private CharacterCounterModel _model = new();
}export default () => ({
message: '',
count: 0,
init() {
// Watch the 'message' property for changes.
this.$watch('message', (newValue) => {
this.count = newValue.length;
});
}
});$refs: Accessing DOM Elements
While reactive data binding is preferred, you sometimes need a direct handle to a DOM element. The $refs magic property allows you to access any element within your component that has an x-ref attribute.
In RizzyUI, $refs is the gateway to component orchestration. By placing an x-ref on another RizzyUI component (like <RzDialog>), you can get its root element and then use the Rizzy.$data() helper to access its Alpine instance and API. This bridges the gap between your Razor components and Alpine, letting you call Alpine APIs on other RizzyUI components.
Preview of Orchestration
We cover this pattern in detail on the Advanced: Orchestration page, but here’s a quick look at the pattern.
My Modal
@attribute [RzAlpineCodeBehind]
@using RizzyUI
<RzAlpineComponent For="this" Name="modalController">
<RzButton x-on:click="openModal">Open Modal</RzButton>
<RzDialog Title="My Modal" x-ref="myModal" />
</RzAlpineComponent>export default () => ({
modalInstance: null,
async init() {
// !IMPORTANT! Allow a tick for modal to transport to body (needed by RzDialog)
// If we don't wait the tick then Rizzy.$data() won't be able to resolve the dialog. Note that waiting a tick
// is only needed to resolve data for components that move themselves in the DOM from template
// (like modals, popovers, etc).
await Alpine.nextTick();
// Get the Alpine instance of the RzDialog component
this.modalInstance = Rizzy.$data(this.$refs.myModal);
},
openModal() {
if (this.modalInstance) {
this.modalInstance.openModal();
}
}
});x-effect: Automatically Reacting to Changes
x-effect is like $watch, but it automatically tracks any reactive properties you use inside it. Whenever any of those dependencies change, the effect re-runs.
When to use $watch vs. x-effect
Use $watch when you need to observe a specific property and react to its change. Use x-effect when you have a block of code that should re-run whenever any of its dependencies change automatically.
A great use case is persisting form data to localStorage as the user types, and showing a status message.
export default () => ({
name: '',
email: '',
status: 'Ready',
init() {
// ... load saved data on initialization ...
// This effect will run whenever 'name' or 'email' changes
this.$effect(() => {
const dataToSave = { name: this.name, email: this.email };
localStorage.setItem('formData', JSON.stringify(dataToSave));
this.status = 'Saving...';
setTimeout(() => { this.status = 'Saved!' }, 500);
});
}
});Global State with Alpine.store()
Sometimes, you need to share state between components that are not parent and child. For this, Alpine provides Alpine.store(). A store is a global reactive object that any component on the page can access using the $store magic property.
This is perfect for application-wide state like a shopping cart count, user authentication status, or the current theme. The <RzDarkModeToggle> component itself uses this pattern internally.
Example: A Global Notification Store
Let's create a simple store to show and hide a site-wide notification banner.
1. Register the Store
This script typically lives in your global site bundle (e.g., site.js) so it runs once at startup.
document.addEventListener('alpine:init', () => {
Alpine.store('notifications', {
message: '',
visible: false,
show(newMessage) {
this.message = newMessage;
this.visible = true;
},
hide() {
this.visible = false;
}
});
});2. Create the Notification Banner
This component reads directly from the store. It can be placed in your main layout file.
<div x-data x-show="$store.notifications.visible" x-transition class="...">
<p x-text="$store.notifications.message"></p>
<RzButton x-on:click="$store.notifications.hide()">Close</RzButton>
</div>3. Trigger the Notification
Any other component on the page can now trigger the notification.
<RzAlpineComponent For="this" Name="updateProfile">
<RzButton x-on:click="$store.notifications.show('Your profile has been updated!')">
Update Profile
</RzButton>
</RzAlpineComponent>Next Steps
You now have the tools to manage both local and global state, react to changes, and interact with the DOM. Next, we'll look at the Lifecycle and Initialization of Alpine components to understand exactly when and how your code runs.