Local state and directives
Choose Alpine-sized problems
Recognize interactions that need local browser state without a full component framework.
Alpine is for the small stuff. A dropdown that opens, a filter that narrows a list, a form field that shows a character count. Interactions that live next to one piece of HTML and never need to leave the browser.
That’s the whole pitch. The server renders the page. Alpine adds behavior where the page needs it. Nothing else changes.
In this course we build an issue board. It’s a server-rendered list of bug reports with a filter panel, an expandable row for each issue, and an inline editor. We’ll add Alpine to it one behavior at a time.
Sort the interactions first
Before writing any Alpine, I list every interaction on the page and ask who owns it. For the board I get four groups:
- server-owned: creating, saving, and deleting issues. The database is the truth.
- URL-owned: which page of results you’re on. Reloading must bring it back.
- page-local: the filter text and the status dropdown. They change what you see, not what exists.
- component-local: whether one issue row is expanded. Nobody else cares.
Alpine belongs in the last two groups. The first two stay with plain HTML forms and links, and later with HTMX.
Start from the HTML that works without JavaScript
Here’s the filter panel before Alpine touches it:
<form action="/issues" method="get">
<label for="query">Search</label>
<input id="query" name="query" type="search">
<label for="status">Status</label>
<select id="status" name="status">
<option value="">All</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
<button>Filter</button>
</form>
Submit it and the server returns the filtered list. That’s a complete feature. Alpine will make it feel instant, but it never becomes the only way to filter.
The mistake to avoid
The mistake I see most often is putting the whole app into x-data. Routing, the list of issues, the logged-in user, everything in one object on <body>. The demo works, so nobody notices until a bug lands in an inline expression nobody can debug.
My rule: Alpine holds only the state one interaction needs. If a value must survive a reload, it belongs in the URL or on the server. If two distant parts of the page need it, stop and ask whether the server should render both.
Take a page you own and write the same four-group list for it. For each interaction, name the owner in one word. The page-local and component-local ones are your Alpine-sized problems.
Lesson completed