Events and forms
Derive state instead of copying it
Compute validity, counts, and filtered views from source state instead of synchronizing duplicate fields.
Some values are facts the user typed. Others are computed from those facts. The remaining character count comes from the title. The visible issue list comes from the issues plus the filters.
Store the facts. Compute the rest. Every time you store a computed value too, you have two things that can disagree.
The copy that drifts
Here’s the mistake, in the issue editor:
<div x-data="{ title: '', remaining: 80 }">
<input x-model="title" @input="remaining = 80 - title.length">
<button @click="title = ''">Clear</button>
<span x-text="remaining"></span>
</div>
Type and the count goes down. Click Clear and the title empties, but remaining still says 73. The Clear button changed the source and forgot the copy.
You can fix it by updating remaining in the Clear handler too. Now there are two places to keep in sync. Add a third button and there are three.
Derive it in the expression
The count is a function of the title. Say so:
<div x-data="{ title: '' }">
<input x-model="title">
<button @click="title = ''">Clear</button>
<span x-text="80 - title.length"></span>
</div>
One source. The span can’t drift because it doesn’t store anything.
Derive it in a getter
When the expression gets longer, move it into a getter, a property that runs code when you read it. Alpine treats getters like any other reactive value:
<div x-data="{
title: '',
get remaining() { return 80 - this.title.length },
get valid() { return this.title.trim().length > 0 && this.remaining >= 0 }
}">
<input x-model="title">
<span x-text="remaining"></span>
<button :disabled="!valid">Save</button>
</div>
valid reads remaining, which reads title. Change the title and both recompute. The Save button enables the moment the title is usable.
The filtered list is derived too
The same rule applies to the board. Don’t keep a second visibleIssues array that you rebuild in handlers. Filter at the point of use:
<template x-for="issue in issues.filter(i => !status || i.status === status)" :key="issue.id">
<li x-text="issue.title"></li>
</template>
Or give it a name with a getter, get visibleIssues(), when the filter logic grows. We’ll come back to x-for in the lists module.
When to break the rule
Derive by default. The exception is a value that’s expensive to compute and read many times per render. That’s rare in Alpine-sized problems. If you think you hit it, measure first.
Try it on your editor: add a duplicated count, make it drift with a second button, then replace it with a getter and watch the bug disappear.
Lesson completed