How do you manage state in Svelte without re-renders or leaks?
Svelte Developer
answer
Efficient Svelte state management starts with granular stores (split by feature), reactive statements for small computations, and setContext/getContext to avoid prop drilling. Prefer derived stores for computed values and custom stores that short-circuit identical updates. Use keyed lists and immutable update patterns to keep diffs predictable. Auto-subscriptions ($store) clean up on destroy; manual subscribe, DOM listeners, timers, and async tasks must be unsubscribed in onDestroy. Co-locate state; promote only what multiple components truly share.
Long Answer
Svelte’s compiler removes the virtual DOM and wires reactivity at build time, so how you organize state strongly affects performance and correctness. The goal is to keep updates small, localized, and intentional—while ensuring every subscription and async handle is cleaned up.
1) Choose the right state home
Use component-local state for UI-only details (open panels, input text). Promote to a store only when multiple components need the same source of truth or when you must share state across route boundaries. This keeps reactivity scoped and prevents accidental global churn.
2) Prefer granular stores over one giant store
Create multiple focused writable stores (e.g., session, cart, filters) instead of a monolith. Each subscriber will then update only when its slice changes. If you must keep related data together, expose faceted selectors via derived stores that project just what consumers need (for example, cartTotal, selectedFilters). Smaller, specific stores mean fewer updates and simpler reasoning.
3) Compute, do not duplicate, with derived
Use derived(a, $a => compute($a)) for values that can be recomputed from other stores. This centralizes logic, avoids per-component $: duplication, and ensures the work runs once per change rather than in every subscriber. When computation is heavy, debounce within a custom derived store or memoize the result; derived stores are lazy and only recompute for active subscribers.
4) Write custom stores that avoid redundant sets
Svelte does not deep-compare store values. Wrap set/update to short-circuit identical values (shallow or deep equality as appropriate) so you do not notify subscribers unnecessarily. For objects/arrays, use immutable update patterns (copy-on-write) to keep equality checks meaningful and to prevent accidental mutation-driven surprises.
5) Use reactive statements ($:) surgically
Svelte runs $: blocks when referenced values change. Keep these blocks small and pure: derive a value, trigger a lightweight effect, or pipe to a store. Guard expensive work with conditions (e.g., if (ready) …) or debounce within the block. For multiple assignments that must be observed together, group updates in the same microtask (Svelte batches automatically) or write to a single store that represents the combined state.
6) Avoid prop drilling with context
Use setContext('key', value) in a provider component (layout, page root) and getContext('key') in descendants to share stores, services, or configuration. Provide stores through context, not snapshots, so subscribers remain reactive. Keep the context surface minimal and stable to avoid churn across trees.
7) Control re-render surfaces in templates
Template updates happen where reactive values are used. Limit wide fan-out by:
- Keyed each-blocks ({#each items as item (item.id)}) to preserve DOM identity and avoid full list re-renders.
- Splitting large components into small boundaries that receive only what they need.
- Passing derived or primitive props rather than whole objects when only one field is used.
- Avoiding excessive reactive expressions inside markup; compute upstream in $:.
8) Async, subscriptions, and cleanup discipline
Auto-subscriptions via $store are cleaned up for you. However, manual store.subscribe calls must be unsubscribed. Return cleanup functions from onMount, and always pair listeners, intervals, observers (Resize/Intersection), and custom event buses with onDestroy to prevent memory leaks. For pending async work, track an abort signal; cancel fetches or ignore stale results when a component unmounts or inputs change.
9) Forms, inputs, and frequent updates
Use bind:value for inputs, but avoid writing to global stores on every keystroke. Buffer changes locally and commit on blur or debounce with a local timer. For frequent animations or drag interactions, keep hot state local and emit coarse-grained updates to stores to reduce global notifications.
10) Routing, SSR, and long-lived singletons
In SvelteKit, be careful with module-scope singletons. If a store outlives a page, ensure it does not retain DOM references or large data blobs. Prefer readable stores for external sources (websocket, event streams) and stop streaming in onDestroy. On the server, never share per-request state across users; create stores per request context.
11) Testing and observability for state
Create pure store modules you can test in isolation. Add minimal dev tooling: log store transitions in development, expose a reset() in tests, and use derived probes to confirm that updates fire only when expected. Measure costly computations, list update counts, and subscription cardinality during profiling to discover hotspots.
12) Patterns that scale
- State slices + selectors (derived) for clarity and minimal fan-out.
- Command/query separation: components read from derived stores, write through small action functions on custom stores.
- Evented side effects: perform I/O in onMount or actions, not in pure derived logic.
- Backpressure: throttle or debounce updates from rapid sources before they hit stores.
Taken together, these strategies yield Svelte apps that remain responsive under load and that free memory promptly when views change.
Table
Common Mistakes
- A single mega-store that forces the whole app to update on tiny changes.
- Writing large objects to a store on every keystroke; thrashing subscribers.
- Using $: for heavy computations per component instead of one derived store.
- Passing entire objects as props when only one primitive field is needed.
- Forgetting to unsubscribe manual subscribe, observers, or event listeners in onDestroy, causing memory leaks.
- Relying on deep mutations (push/splice) without reassigning, so views never update—or reassigning huge objects too often, causing extra updates.
- Unkeyed {#each} lists that reorder nodes and trigger full re-renders.
- Providing snapshots through context (non-reactive), breaking downstream updates.
Sample Answers
Junior:
“I use small writable stores per feature and $: for tiny derived values. I key my {#each} loops and keep forms local, only committing to a store on blur. I use $store so subscriptions are cleaned up automatically.”
Mid:
“I introduce derived selectors so heavy computations run once, and I wrap stores to short-circuit identical updates. Shared dependencies are passed via setContext/getContext to avoid prop drilling. For async, I return cleanups from onMount and cancel pending fetches with an AbortController in onDestroy.”
Senior:
“I design a slice/selector architecture, command/query separation, and custom stores with equality checks and debounced updates for hot paths. I stabilize templates with keyed lists and narrow props. I audit subscriptions, observers, and timers to guarantee zero leaks, and I profile list updates to cap fan-out. This keeps Svelte apps fast and maintainable.”
Evaluation Criteria
Look for granular stores, not a single global blob; derived selectors for computed values; and custom stores that suppress identical updates. Strong answers mention context to avoid prop drilling, keyed lists, immutable updates, and guarded $: blocks. They should explicitly call out cleanup with onDestroy for manual subscriptions, listeners, timers, and async work. Bonus points for debouncing hot inputs, aborting fetches, and separating read selectors from write commands. Red flags: unkeyed lists, deep mutations without reassignment, no cleanup strategy, passing entire objects through many layers, or scattering heavy $: computations across components.
Preparation Tips
- Build a small app with three slices (session, cart, filters) and promote only shared state to stores.
- Replace repeated $: math with one derived selector; measure recompute counts.
- Implement a custom writable that shallow-compares and skips redundant set.
- Convert a prop-drilled dependency to context; verify subscribers re-render only when needed.
- Add keyed {#each} and switch to immutable updates; profile list churn.
- Create an onMount fetch with AbortController; cancel on route change to prove no leaks.
- Write a checklist for onDestroy: unsubscribe store, remove listeners/observers, clear timers, cancel async.
- Instrument a dev logger that counts store notifications to spot chatty updates early.
Real-world Context
A marketplace split a chatty global store into slices and added derived selectors for totals and badges; notifications dropped 70% and time-to-interactive stabilized under load. A dashboard with heavy lists switched to keyed {#each} and immutable updates; reordering no longer re-rendered entire sections. A forms app buffered input locally and committed debounced changes; CPU spikes vanished during typing. Another team audited manual subscribe calls and event listeners, moving cleanups to onDestroy and canceling fetches with AbortControllers; memory stayed flat across route churn. Each win came from smaller update surfaces and disciplined cleanup.
Key Takeaways
- Keep state local by default; promote to stores only when shared.
- Use derived selectors for computations and custom stores to suppress identical updates.
- Stabilize templates with keyed lists and narrow, primitive props.
- Guard $: blocks and buffer hot inputs locally.
- Clean up everything in onDestroy: subscriptions, listeners, timers, async.
Practice Exercise
Scenario:
You are building a Svelte catalog page with filters, a paginated list, and a cart sidebar. Scrolling and typing feel laggy, and navigating between routes grows memory over time.
Tasks:
- Split state into filters, catalog, and cart stores. Expose derived selectors for activeCount, visibleItems, and cartTotal.
- Wrap each store with an equality check so set no-ops on identical values; ensure updates use immutable patterns.
- Replace repeated per-component $: computations with shared derived selectors.
- Convert prop-drilled dependencies to context: provide { filters, catalog } stores at the layout level, consume them in list and sidebar components.
- Key all item loops by stable IDs; pass only the primitive props each item cell needs.
- For search input, hold text locally and debounce commits to filters to 200 ms.
- In onMount, fetch pages with AbortController; cancel on input change or onDestroy. Remove manual subscriptions and listeners in onDestroy.
- Add a simple dev logger to count store notifications. Optimize until typing and scroll remain smooth and notifications drop measurably.
Deliverable:
A refactored catalog where updates are granular, renders are stable, and route changes leave no memory leaks, demonstrated by lower notification counts and steady memory usage.

