
Reactive Ui Patterns
- 2 installs
- 6 repo stars
- Updated August 3, 2026
- spences10/devhub-crm
Shows remote-function reactive UI patterns using the .current property for smooth in-place updates without page jumps.
About
Documents Svelte remote-function patterns that store queries in variables and use .current for in-place updates that preserve scroll position. A developer uses it to avoid jarring reloads when refreshing data.
- Store queries in a variable to access the .current property
- Show a spinner only on initial load when .current is undefined
Reactive Ui Patterns by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,862 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/devhub-crm --skill reactive-ui-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 6 |
| Last updated | August 3, 2026 |
| Repository | spences10/devhub-crm ↗ |
What it does
Shows remote-function reactive UI patterns using the .current property for smooth in-place updates without page jumps.
Files
Reactive UI Patterns
Quick Start
<script lang="ts">
const data_query = get_data(); // Store in variable for .current access
async function save(id: string, value: string) {
await update_data({ id, value });
await data_query.refresh(); // Updates in place!
}
</script>
{#if data_query.error}
<p>Error loading data</p>
{:else if data_query.loading && data_query.current === undefined}
<p>Loading...</p>
{:else}
{@const items = data_query.current ?? []}
<div class:opacity-60={data_query.loading}>
{#each items as item}<!-- Content updates smoothly -->{/each}
</div>
{/if}Core Principles
- Store queries:
const query = get_data()enables.current
property access
- Use `.current`: Prevents page jumps, keeps scroll position
during updates
- Initial load only: Show spinner when
.current === undefined,
not on every refresh
- Avoid `{#await}`: Causes jarring page reloads - use stored query
pattern instead
Reference Files
- current-property.md - Deep dive on
.current property
- anti-patterns.md - Common mistakes to
avoid
- examples.md - Real-world implementation
examples
Reactive Ui Patterns
Remote functions reactive UI patterns. Use for smooth in-place updates, preventing page jumps, and managing loading states with .current property.
Structure
SKILL.md- Main skill instructionsreferences/- Detailed documentation loaded as neededscripts/- Executable code for deterministic operationsassets/- Templates, images, or other resources
Usage
This skill is automatically discovered by Claude when relevant to the task.
Anti-Patterns to Avoid
1. Manual Refresh Keys
❌ DON'T:
let refresh_key = $state(0);
{#key refresh_key}
{#await get_data() then data}
<!-- Forces full re-render -->
{/await}
{/key}✅ DO: Let remote functions handle refresh automatically
2. Using {#await} for Inline Editing
❌ DON'T:
{#await get_data() then items}
<!-- Re-renders entire block, causes page jump -->
{/await}✅ DO: Use .current property pattern
3. Hiding Content During Refresh
❌ DON'T:
{#if data_query.loading}
<p>Loading...</p>
<!-- Hides on every refresh -->
{/if}✅ DO: Only hide when .current === undefined
4. Manual Refresh Callbacks
❌ DON'T:
<Component on_change={() => query.refresh()} />✅ DO: Remote functions refresh automatically
5. Using window.location.reload()
❌ DON'T: Force page reload - defeats reactivity
✅ DO: Use query.refresh() for surgical updates
The .current Property
The .current property retains previous data during refresh, enabling smooth in-place updates without page jumps.
The Problem: Page Jumps with {#await}
When using {#await query()} with .refresh(), Svelte re-renders the entire block, causing:
- Page scrolls to top - User loses their scroll position
- Content disappears then reappears - Jarring visual flash
- Component state is lost - Edit forms reset, selections clear
- Component structure recreated - Expensive DOM operations
The Solution: .current Property
Store the query in a variable and access .current to:
- Keep previous data visible during refresh
- Preserve scroll position - No component recreation
- Maintain component state - Forms, selections stay intact
- Show subtle loading indicators - Optional opacity instead of
full spinner
Query Object Properties
const query = get_data();
query.loading; // boolean - true when fetching
query.error; // Error | null - error state
query.current; // T | undefined - persists during refresh!The Three States
1. Initial load (.current === undefined + .loading === true):
- Show full loading spinner
- No previous data available
- User expects to wait
2. During refresh (.current has data + .loading === true):
- Keep showing previous data
- Add optional opacity-60 to indicate loading
- User can continue interacting with current data
3. After refresh (.current updated + .loading === false):
- Display fresh data
- Remove loading indicator
- Smooth transition - no page jump
❌ Bad Pattern: Using {#await} for Inline Editing
<script lang="ts">
import { get_interactions, update_interaction } from './interactions.remote';
let edit_id = $state<string | null>(null);
async function save_edit() {
await update_interaction({ id: edit_id, ... });
edit_id = null;
await get_interactions().refresh(); // ⚠️ Causes page jump!
}
</script>
<!-- ❌ Re-renders entire block on .refresh() -->
{#await get_interactions() then interactions}
{#each interactions as interaction}
{#if edit_id === interaction.id}
<input bind:value={interaction.note} />
<button onclick={save_edit}>Save</button>
{:else}
<p>{interaction.note}</p>
<button onclick={() => (edit_id = interaction.id)}>Edit</button>
{/if}
{/each}
{/await}What happens on save:
1. User clicks Save 2. .refresh() is called 3. Entire {#await} block re-renders from scratch 4. Page jumps to top (scroll position lost) 5. Edit form disappears then reappears 6. Jarring UX - feels like page reload
✅ Good Pattern: Using .current Property
<script lang="ts">
import { get_interactions, update_interaction } from './interactions.remote';
// Store query in variable to access .current
const interactions_query = get_interactions();
let edit_id = $state<string | null>(null);
async function save_edit() {
await update_interaction({ id: edit_id, ... });
edit_id = null;
await interactions_query.refresh(); // ✅ Smooth in-place update!
}
</script>
<!-- ✅ Only show spinner on INITIAL load -->
{#if interactions_query.error}
<p>Error loading interactions</p>
{:else if interactions_query.loading && interactions_query.current === undefined}
<p>Loading interactions...</p>
{:else}
{@const interactions = interactions_query.current ?? []}
<!-- Subtle opacity during refresh, content stays visible -->
<div class:opacity-60={interactions_query.loading}>
{#each interactions as interaction}
{#if edit_id === interaction.id}
<input bind:value={interaction.note} />
<button onclick={save_edit}>Save</button>
{:else}
<p>{interaction.note}</p>
<button onclick={() => (edit_id = interaction.id)}
>Edit</button
>
{/if}
{/each}
</div>
{/if}What happens on save:
1. User clicks Save 2. .refresh() is called 3. Content stays visible with opacity-60 4. Data updates in place - no scroll jump 5. Smooth transition to updated content 6. Great UX - feels instant and responsive
Why .current is Better
| Aspect | {#await} | .current |
|---|---|---|
| Scroll position | Lost - scrolls to top | Preserved - stays in place |
| Content visibility | Hides then shows (flash) | Always visible |
| Component state | Lost - inputs reset | Preserved - forms stay intact |
| Loading indicator | Full spinner blocks content | Subtle opacity, content accessible |
| User experience | Feels like page reload | Feels smooth and responsive |
| DOM operations | Recreates entire component tree | Updates in place |
| Edit state | Lost during refresh | Maintained during refresh |
When to Use .current
Always use `.current` for:
- Inline editing - Forms within lists
- Real-time updates - Data that refreshes frequently
- Infinite scroll - Adding items to existing list
- Optimistic UI - Immediate feedback before server confirmation
- Search/filter - Updating results without hiding current data
- Dashboards - Multiple widgets that refresh independently
Can use `{#await}` for:
- Initial page load - No previous state to preserve
- Full page navigation - User expects full transition
- Modal content - Isolated from main page scroll
Complete Pattern Example
<script lang="ts">
import {
get_contacts,
update_contact,
delete_contact,
} from './contacts.remote';
const contacts_query = get_contacts();
let editing_id = $state<string | null>(null);
let edit_name = $state('');
function start_edit(contact: Contact) {
editing_id = contact.id;
edit_name = contact.name;
}
async function save_edit() {
if (!editing_id) return;
await update_contact({ id: editing_id, name: edit_name });
editing_id = null;
await contacts_query.refresh(); // Smooth update!
}
async function handle_delete(id: string) {
if (!confirm('Delete contact?')) return;
await delete_contact(id);
await contacts_query.refresh(); // Smooth update!
}
</script>
{#if contacts_query.error}
<div class="alert alert-error">
<p>Failed to load contacts: {contacts_query.error.message}</p>
<button onclick={() => contacts_query.refresh()}>Retry</button>
</div>
{:else if contacts_query.loading && contacts_query.current === undefined}
<!-- Only show full spinner on initial load -->
<div class="flex items-center gap-2">
<span class="loading loading-spinner"></span>
<span>Loading contacts...</span>
</div>
{:else}
{@const contacts = contacts_query.current ?? []}
<!-- Subtle loading indicator during refresh -->
<div class:opacity-60={contacts_query.loading}>
{#each contacts as contact (contact.id)}
<div class="card bg-base-100 p-4 shadow-md">
{#if editing_id === contact.id}
<!-- Edit mode -->
<input bind:value={edit_name} class="input w-full" />
<div class="mt-2 flex gap-2">
<button
class="btn btn-sm btn-primary"
onclick={save_edit}
>
Save
</button>
<button
class="btn btn-ghost btn-sm"
onclick={() => (editing_id = null)}
>
Cancel
</button>
</div>
{:else}
<!-- View mode -->
<p class="text-lg font-semibold">{contact.name}</p>
<div class="mt-2 flex gap-2">
<button
class="btn btn-ghost btn-sm"
onclick={() => start_edit(contact)}
>
Edit
</button>
<button
class="btn btn-sm btn-error"
onclick={() => handle_delete(contact.id)}
>
Delete
</button>
</div>
{/if}
</div>
{/each}
</div>
<!-- Optional: Show refresh indicator -->
{#if contacts_query.loading}
<div class="mt-2 text-sm opacity-60">Refreshing...</div>
{/if}
{/if}Key Takeaways
1. Store queries in variables - const query = get_data() enables .current access 2. Check `.current === undefined` - Only show spinner on initial load 3. Use opacity during refresh - Keep content visible and accessible 4. Preserve user state - Forms, selections, scroll position all maintained 5. Better UX - Smooth transitions instead of jarring page jumps
Real-World Examples
Example 1: Inline Contact Editing
<script lang="ts">
import { get_contacts, update_contact } from './contacts.remote';
const contacts_query = get_contacts();
let edit_id = $state<string | null>(null);
</script>
{#if contacts_query.error}
<p>Error loading contacts</p>
{:else if contacts_query.loading && contacts_query.current === undefined}
<p>Loading...</p>
{:else}
{@const contacts = contacts_query.current ?? []}
<div class:opacity-60={contacts_query.loading}>
{#each contacts as contact}
{#if edit_id === contact.id}
<!-- Edit mode - smooth save without page jump -->
{:else}
<!-- View mode -->
{/if}
{/each}
</div>
{/if}Example 2: Social Links Manager
<script lang="ts">
import {
get_social_links,
add_social_link,
delete_social_link,
} from './profile.remote';
const social_links = get_social_links();
</script>
{#await social_links then links}
<SocialLinksManager
social_links={links || []}
on_add={async (platform, url) => {
await add_social_link({ platform, url });
social_links.refresh(); // ✅ Smooth update
}}
on_delete={async (link_id) => {
await delete_social_link(link_id);
social_links.refresh(); // ✅ Smooth update
}}
/>
{/await}Example 3: Interaction Notes
<script lang="ts">
import {
get_interactions,
update_interaction,
} from './interactions.remote';
const interactions_query = get_interactions();
</script>
{#if interactions_query.loading && interactions_query.current === undefined}
<span class="loading loading-spinner"></span>
{:else}
{@const interactions = interactions_query.current ?? []}
<div class:opacity-60={interactions_query.loading}>
{#each interactions as interaction}
<!-- Inline edit without page jumps -->
{/each}
</div>
{/if}