Optimistic Update with data-query

Let write() apply the change now and roll it back for you if the server says no.

The plain Optimistic Update pattern keeps the saving flag, the revert and the error message in component state by hand. When the list comes from a data-query (Full build, v1.5 and later), the query already does that work: a write appears on screen the moment you call it, a rejection undoes exactly the fields that write changed, and the query counts the writes still in flight.

Live Demo

The demo runs in its own frame on the Full build. Open it on its own ↗

Source

HTML + JavaScript
<div data-component="task-list">
    <p data-show="$tasks.isLoading">Loading tasks…</p>
    <div data-list="tasks" data-key="id">
        <template>
            <div>
                <button data-action="toggle" data-bind="done ? 'Undo' : 'Done'"></button>
                <span data-bind="label" data-bind-class="done ? 'done' : ''"></span>
            </div>
        </template>
    </div>
    <p data-show="$tasks.pendingWrites > 0">Saving…</p>
    <p data-show="lastError" data-bind="lastError"></p>
</div>

<script>
wildflower.query('tasks', {
    from: () => fetch('/api/tasks').then(r => r.json()),
    key: 'id',
    // Runs after write() has already applied the change on screen.
    // Resolve with the saved record and the row is confirmed; reject
    // (or answer with a 4xx/5xx) and the claimed fields roll back.
    to: item => fetch('/api/tasks/' + item.id, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item)
    }).then(r => r.json())
});

wildflower.component('task-list', {
    state: { lastError: '' },
    computed: {
        tasks() { return wildflower.getQuery('tasks').rows; }
    },
    async toggle(event, element, { item }) {
        this.lastError = '';
        try {
            // On screen immediately; settles when the server answers.
            await wildflower.getQuery('tasks').write({ id: item.id, done: !item.done });
        } catch (e) {
            // The rollback has already happened; only the message is left.
            this.lastError = item.label + ': ' + e.message + '. Reverted.';
        }
    }
});
</script>

Key Points

  • write({ id, done }) merges into the row immediately, so the user sees the change before the request is even sent; the request runs through the query's to
  • A rejected write, or a not-ok response from a URL destination, rolls back on its own, and only the fields that write claimed: a rename in flight on the same row survives a failed checkbox
  • $tasks.pendingWrites is the saving indicator for the whole list; it rises as writes dispatch and falls as each settles, success or failure
  • write() returns a promise, so a plain try/catch is where the message comes from; the failure is also on $tasks.syncError, and the rows never go blank
  • Resolving to with the saved record confirms the row with no second request; resolving with nothing makes the query refetch instead
  • Needs the Full build. On the other tiers, or where the list is component state, use the plain pattern; the docs cover the rules in Optimistic Updates and Rollback and Writes