# Day 5: Request State, Concurrency, Active Search, and Race-Safe Operations

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Turn ordinary requests into dependable daily operations by using htmx’s factual request state, indicators, disabling, debounce, synchronization, queue policy, and explicit retry behavior.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- display loading and busy state without a duplicate Alpine `loading` flag;
- disable the right controls for the right request;
- use trigger delay and `hx-sync` for different concurrency problems;
- design active search that cancels obsolete requests;
- distinguish request state from broader local workflow state;
- define failure, retry, and idempotency behavior.

## 1. The factual request state belongs to htmx

htmx knows when its request begins and ends. Use that truth before inventing another state machine.

Primary tools:

- `hx-indicator`;
- the `htmx-request` CSS class;
- `hx-disabled-elt`;
- `hx-sync`;
- htmx lifecycle events;
- server-rendered progress or error HTML.

A duplicate Alpine boolean can drift when a request aborts, is replaced, fails before send, or is triggered by another element.

## 2. Indicator SOP

```html
<form
    hx-post="/profile"
    hx-target="#profile"
    hx-indicator="#profile-spinner"
>
    <label>
        Display name
        <input name="display_name">
    </label>

    <button type="submit">Save</button>

    <span
        id="profile-spinner"
        class="htmx-indicator"
        role="status"
        aria-live="polite"
    >
        Saving…
    </span>
</form>
```

Example CSS:

```css
.htmx-indicator {
    display: none;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
    display: inline;
}
```

Rules:

1. Put status text near the operation it describes.
2. Use a live region only when the announcement is useful.
3. Do not hide all page content behind a full-screen spinner for a small request.
4. Keep the original control label stable when possible; changing it can confuse screen-reader users.
5. For long-running durable work, render server-owned progress rather than pretending one HTTP request is the whole workflow.

## 3. Disabling controls correctly

Use `hx-disabled-elt` to prevent accidental repeated activation.

```html
<form
    hx-post="/checkout"
    hx-target="#checkout"
    hx-indicator="#checkout-progress"
    hx-disabled-elt="find fieldset, find button[type='submit']"
>
    <fieldset>
        <!-- all checkout controls -->
    </fieldset>

    <button type="submit">Pay</button>
    <p id="checkout-progress" class="htmx-indicator" role="status">
        Processing…
    </p>
</form>
```

Why this is dependable:

- a disabled fieldset disables its descendant controls;
- the submit button is explicitly disabled;
- the selector does not pretend `find input` means every input.

Do not disable navigation or cancellation controls that must remain usable unless the workflow explicitly requires it.

## 4. Three different timing problems

### Problem A: too many events before a request

Use trigger `delay` or `throttle`.

```html
<input
    name="q"
    hx-get="/search"
    hx-trigger="input changed delay:300ms"
    hx-target="#results"
>
```

`delay` waits for a quiet period. It is debounce.

### Problem B: a newer request makes an older request obsolete

Use `hx-sync="this:replace"` or synchronize on a shared owner.

```html
<input
    name="q"
    hx-get="/search"
    hx-trigger="input changed delay:300ms"
    hx-target="#results"
    hx-sync="this:replace"
>
```

A new request aborts the current request and replaces it.

### Problem C: related elements issue competing requests

Synchronize them on a common element.

```html
<form
    hx-post="/account"
    hx-target="#account-form"
    hx-sync="this:replace"
>
    <input
        name="username"
        hx-post="/account/validate-username"
        hx-trigger="change"
        hx-target="#username-error"
        hx-sync="closest form:abort"
    >

    <div id="username-error"></div>
    <button type="submit">Save</button>
</form>
```

The input validation yields to the form submission.

## 5. `hx-sync` strategies

| Strategy | Behavior | Typical use |
| --- | --- | --- |
| `drop` | ignore a new request while one is active | protect an operation that must finish |
| `abort` | ignore this request if another is active; allow another request to abort it | low-priority validation |
| `replace` | abort current request and issue newest | active search, rapid filters |
| `queue first` | keep first waiting request | first follow-up matters |
| `queue last` | keep latest waiting request | latest adjustment matters |
| `queue all` | execute every queued request | rare; every action is meaningful |

Choose by business meaning. Do not select `queue all` merely to avoid thinking about races.

## 6. Active search pattern

A robust search has four kinds of state:

- URL: applied query and filters;
- controls: current values;
- htmx: debounce, request, replacement, history;
- Alpine: local filter-panel visibility or density.

```html
<section x-data="{ filtersOpen: false, density: 'comfortable' }">
    <button
        type="button"
        @click="filtersOpen = !filtersOpen"
        :aria-expanded="filtersOpen"
        aria-controls="search-filters"
    >
        Filters
    </button>

    <form
        id="search-form"
        x-cloak
        x-show="filtersOpen"
        action="/search"
        method="get"
        hx-get="/search"
        hx-trigger="
            input changed delay:300ms from:#search-q,
            change from:#search-status,
            submit
        "
        hx-target="#search-results"
        hx-push-url="true"
        hx-sync="this:replace"
        hx-indicator="#search-progress"
    >
        <label>
            Search
            <input id="search-q" type="search" name="q">
        </label>

        <label>
            Status
            <select id="search-status" name="status">
                <option value="">Any</option>
                <option value="open">Open</option>
                <option value="closed">Closed</option>
            </select>
        </label>

        <button type="submit">Search</button>
        <span id="search-progress" class="htmx-indicator" role="status">
            Updating results…
        </span>
    </form>

    <div
        :class="density === 'compact'
            ? 'results results--compact'
            : 'results results--comfortable'"
    >
        <div id="search-results">
            <!-- canonical server-rendered result set -->
        </div>
    </div>
</section>
```

Why exact IDs are used in the trigger:

- `from:` selectors can be broader than the visual component;
- explicit controls make the contract visible;
- newly introduced controls require a deliberate update to the trigger contract.

An alternative is to put htmx attributes directly on each triggering control with `hx-include="#search-form"`.

## 7. Stale-result protection

For ordinary active search, `hx-sync="this:replace"` is the default starting point. Still test:

1. type rapidly;
2. make the server respond out of order;
3. navigate back while a request is active;
4. submit with Enter;
5. clear the search with the browser’s search-field clear control;
6. change a select while text debounce is pending.

The displayed results and URL must represent the same applied request.

## 8. When Alpine may own request-adjacent state

Alpine may own a broader workflow that is not equivalent to “request in flight.”

Examples:

- whether a progress drawer is open;
- a reversible provisional highlight;
- a local confirmation step;
- whether the user has paused presentation updates;
- whether a retry explanation is expanded.

```html
<section x-data="{ detailsOpen: false }">
    <button type="button" @click="detailsOpen = !detailsOpen">
        Explain this operation
    </button>

    <div x-cloak x-show="detailsOpen">
        This explanation is local and remains usable while a request runs.
    </div>

    <button
        hx-post="/reports/rebuild"
        hx-indicator="#rebuild-progress"
        hx-disabled-elt="this"
    >
        Rebuild report
    </button>

    <span id="rebuild-progress" class="htmx-indicator">
        Starting rebuild…
    </span>
</section>
```

Do not call the Alpine value `loading`. Name it by its real meaning.

## 9. Idempotency and repeated activation

UI disabling is not a transaction guarantee. For sensitive actions, the server should use appropriate safeguards:

- idempotency keys;
- transaction constraints;
- unique operation identifiers;
- optimistic locking;
- duplicate detection;
- safe retry semantics.

A payment remains server-sensitive even if the button becomes disabled.

For an idempotent refresh, retry can be straightforward. For a non-idempotent mutation, retry policy must be explicit.

## 10. Network failure policy

Define a shared policy for:

- offline state;
- timeout;
- retryable GET requests;
- non-retryable or ambiguous mutations;
- preserved form drafts;
- session expiry;
- unexpected status codes;
- user-visible status announcements.

A narrow htmx 2 listener may add a generic network banner:

```html
<script>
document.body.addEventListener('htmx:sendError', () => {
    document.dispatchEvent(new CustomEvent('network-unavailable'))
})
</script>
```

Keep global handlers cross-cutting. They must not contain feature-specific success logic.

Event names differ in htmx 4; this is version-sensitive integration code.

## 11. Long-running jobs

A request that starts a job is not the same as the job being complete.

```html
<form
    hx-post="/exports"
    hx-target="#export-job"
    hx-swap="outerHTML"
>
    <button type="submit">Start export</button>
</form>

<section id="export-job">
    No export running.
</section>
```

The server may return:

```html
<section
    id="export-job"
    hx-get="/exports/8/status"
    hx-trigger="every 2s"
    hx-target="this"
    hx-swap="outerHTML"
>
    <p>Export queued.</p>
</section>
```

When complete, the server returns the same root without the polling attributes and includes the download link.

Alpine may own whether details are expanded. It must not own the factual job status.

## 12. Request-state review SOP

For every interaction, write:

```text
Can events arrive faster than requests complete?
Which request should win?
Can the operation be repeated safely?
What becomes disabled?
What remains usable?
Where is progress announced?
What happens on timeout?
What happens if the response is obsolete?
What happens after history navigation?
Is there a durable job after the initiating request?
```

## 13. Day lab: race-safe product search

Build an active product search with:

- search text;
- category and availability filters;
- 300 ms debounce;
- request replacement;
- URL updates;
- local filter-panel visibility;
- local density preference;
- a result count rendered by the server;
- a network error banner;
- direct URL support.

Use browser devtools to throttle the network and produce out-of-order responses.

## Mini-tasks

### Task A: choose the sync strategy

Select a strategy for:

- active search;
- repeated “refresh report” clicks;
- username validation versus final submit;
- a sequence of audit events where every event must be recorded;
- a quantity stepper where only the final quantity matters.

### Task B: remove duplicate loading state

Refactor an Alpine component with `loading = true/false` around an htmx request to use htmx state.

### Task C: disable safely

Repair `hx-disabled-elt="find input, find select, find button"` on a form with many controls.

### Task D: distinguish request and job state

Model an export request whose HTTP response only confirms that a server job started.

## Comprehension check

1. Why is htmx the authority for request-in-flight state?
2. What problem does trigger delay solve?
3. What different problem does `hx-sync` solve?
4. When is `replace` appropriate?
5. Why is disabling a button not enough for payments?
6. What state belongs in the URL for active search?
7. What may Alpine own beside active search?
8. How should a completed polling component stop polling?
9. What makes a global lifecycle listener appropriate?
10. Why must retry behavior differ for GET and ambiguous mutations?

## Cheat sheet

```text
TOO MANY PRE-REQUEST EVENTS      -> delay / throttle
NEWEST REQUEST SHOULD WIN        -> hx-sync="...:replace"
LOW-PRIORITY VALIDATION          -> ...:abort
REQUEST BUSY                     -> hx-indicator / htmx-request
DISABLE MANY FORM CONTROLS       -> disabled fieldset + explicit button
HTTP REQUEST STARTED A JOB       -> poll/stream server-owned job HTML
DUPLICATE SERVER OPERATION       -> server idempotency, not UI hope
```

## Exit exercise

Create a “recalculate forecast” workflow. The initial mutation starts a job, the status component polls, a stable Alpine panel may remain open, and repeated activation is prevented both in the UI and on the server. Document how timeout, retry, and duplicate submission behave.

## Exit criteria

You pass when slow, repeated, aborted, and out-of-order requests produce the same authoritative result as the happy path.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.
