When to Use What
Svelte gives you multiple tools for managing state: $state, props, context, stores, and URL parameters. Each solves a different problem. The mistake most developers make is reaching for a complex solution before they need it. This topic provides a clear decision framework so you pick the right tool the first time.
The Decision Tree
Start at the top and stop at the first match:
- Is the state local to one component? Use
$state. - Does a parent need to pass data to a direct child? Use props.
- Does data need to flow from a child back to a parent? Use callback props or
$bindable. - Does state need to be shared within a specific component subtree? Use context.
- Does state need to be accessible from anywhere in the app, including non-component code? Use a store (or a
.svelte.tsmodule). - Should the state survive page navigation or be shareable via URL? Use URL parameters.
Most applications need fewer state management tools than you think. A surprising amount of state fits in categories 1-3.
$state: Component-Local State
The most common kind of state. A counter, a form input, a toggle, a selected tab. It lives in one component and nowhere else.
<script>
let isOpen = $state(false);
let searchQuery = $state('');
let selectedItems = $state<number[]>([]);
</script>
If the state does not need to be seen by other components, $state is the answer. Full stop. Do not promote it to a store or context "just in case." You can always refactor later if requirements change, and they usually do not.
Derived State
If a value can be computed from other state, use $derived:
<script>
let items = $state([/* ... */]);
let filter = $state('all');
let filteredItems = $derived(
filter === 'all' ? items : items.filter(i => i.status === filter)
);
let count = $derived(filteredItems.length);
</script>
$derived is not a separate category of state management. It is a natural extension of $state. If you find yourself writing $effect to keep one $state variable in sync with another, that should be $derived.
Props: Parent-to-Child
When a parent component has data that a child needs to render or operate on, pass it as a prop:
<!-- Parent.svelte -->
<script>
import UserCard from './UserCard.svelte';
let user = $state({ name: 'Alice', email: 'alice@example.com' });
</script>
<UserCard name={user.name} email={user.email} />
<!-- UserCard.svelte -->
<script>
let { name, email } = $props();
</script>
<div class="card">
<h3>{name}</h3>
<p>{email}</p>
</div>
Props are the simplest, most traceable form of data sharing. You can see exactly where data comes from by reading the template. Do not avoid props because "prop drilling is bad." Prop drilling through two or three levels is fine. It is explicit and easy to follow.
Callbacks & $bindable: Child-to-Parent
When a child needs to communicate back to the parent:
<!-- Callback pattern -->
<SearchInput onsearch={(q) => query = q} />
<!-- Bindable pattern (for form-like components) -->
<ColorPicker bind:value={selectedColor} />
Callbacks for events and actions. $bindable for two-way data synchronization with form elements.
Context: Tree-Wide Sharing
Context solves the problem of passing data through many intermediate components that do not use it. If you have a layout component wrapping a sidebar wrapping a nav wrapping menu items, and the menu items need the current user, threading user through every layer is tedious.
<!-- +layout.svelte -->
<script>
import { setContext } from 'svelte';
let { data, children } = $props();
setContext('user', data.user);
</script>
{@render children()}
<!-- Deeply nested component -->
<script>
import { getContext } from 'svelte';
const user = getContext('user');
</script>
<span>Hello, {user.name}</span>
Good candidates for context:
- Authentication state: The current user, permissions, login/logout functions.
- Theme configuration: Colors, spacing, component variants.
- Locale & translations: Current language, translation function.
- Form state: Validation errors, submit handler, shared across form fields.
Context is per-component-tree. If you render the same provider twice, each subtree gets its own instance. This is a feature, not a limitation.
When Context Is Wrong
If you need to access the state from a utility function, an API module, or a component that is not a descendant of the provider, context will not work. Use a store.
Stores: Truly Global State
Stores are for state that must be accessible from anywhere, including:
- Non-component code (API modules, utility functions).
- Components in completely different parts of the tree with no shared ancestor provider.
- Server-side code that initializes state.
// src/lib/stores/notifications.ts
import { writable } from 'svelte/store';
interface Notification {
id: number;
message: string;
type: 'info' | 'error' | 'success';
}
function createNotificationStore() {
const { subscribe, update } = writable<Notification[]>([]);
let nextId = 0;
return {
subscribe,
add: (message: string, type: Notification['type'] = 'info') => {
const id = nextId++;
update(n => [...n, { id, message, type }]);
setTimeout(() => {
update(n => n.filter(item => item.id !== id));
}, 5000);
}
};
}
export const notifications = createNotificationStore();
// src/lib/api.ts (not a component!)
import { notifications } from '$lib/stores/notifications';
export async function saveItem(item: Item) {
const response = await fetch('/api/items', {
method: 'POST',
body: JSON.stringify(item)
});
if (!response.ok) {
notifications.add('Failed to save item', 'error');
throw new Error('Save failed');
}
notifications.add('Item saved', 'success');
return response.json();
}
The key indicator for a store: you need to read or write the state from code that is not a Svelte component or is not inside the relevant component tree.
Stores & SSR Warning
On the server, module-level stores are shared across all requests. If you set a store value during one user's request, another user might see it. For SSR-safe global state, use context in your root layout instead, or ensure stores are only modified on the client.
URL Parameters: Page State
State that belongs in the URL includes:
- Current page, tab, or section.
- Search queries and filters.
- Sort order.
- Pagination offset.
<!-- src/routes/products/+page.svelte -->
<script>
import { page } from '$app/state';
// Read from URL
let search = $derived(page.url.searchParams.get('q') ?? '');
let sortBy = $derived(page.url.searchParams.get('sort') ?? 'name');
</script>
<form method="GET">
<input name="q" value={search} placeholder="Search products...">
<select name="sort">
<option value="name" selected={sortBy === 'name'}>Name</option>
<option value="price" selected={sortBy === 'price'}>Price</option>
</select>
<button type="submit">Apply</button>
</form>
URL state has unique benefits:
- Shareable: Users can copy the URL and share their exact view.
- Bookmarkable: Browser bookmarks preserve the state.
- Navigable: The back button works.
- SSR-compatible: The server can read URL parameters and render the correct page.
If you are storing filter state in a store when it belongs in the URL, you lose all of these benefits.
Real-World Example: E-Commerce App
Here is how these tools map to a typical e-commerce application:
$state (component-local):
- Is the mobile menu open?
- What text is in the search input right now?
- Is this accordion expanded?
- Form field values before submission.
Props (parent-child):
- Product data passed to a ProductCard.
- Cart item passed to a CartRow.
- Handlers like onAddToCart, onRemove.
Context (tree-wide):
- Current user/auth state (set in layout, used in header/menu/checkout).
- Theme/locale (set at app root).
- Shopping cart (set in layout, used in header badge and cart page).
Stores (global):
- Toast notifications (triggered from API utilities and displayed in a fixed overlay).
- Feature flags (loaded once, read everywhere).
URL parameters (page state):
- /products?category=shoes&sort=price&page=2
- /search?q=running+shoes
- /products/[id] (the product being viewed)
The Escalation Pattern
Start simple and escalate only when you hit a real limitation:
- Start with $state. Most state is local.
- Pass props when a child needs the data. Two or three levels of prop passing is fine.
- Add context when prop drilling becomes painful. Usually at four or more levels, or when intermediate components do not use the data.
- Use a store when context cannot reach. Non-component code, disconnected trees, truly global state.
- Use URL params for navigational state. Filters, pagination, search queries, active tabs.
Resist the urge to start with stores for everything. The simplest solution that works is the right solution. You can always refactor to a more powerful tool when the need is proven.
Common Pitfalls
- Putting everything in stores: This is the Svelte equivalent of putting everything in Redux. Most state does not need to be global. Stores make state harder to trace because any component can modify them from anywhere.
- Avoiding prop drilling too aggressively: Two levels of prop passing is not "drilling." It is explicit, traceable data flow. Do not add context or a store to avoid a single intermediate prop.
- Forgetting URL state: If a user cannot share their current view via URL, that is often a bug. Search queries, filters, sort orders, and pagination belong in URL parameters.
- Using stores for state that belongs in context: Authentication state, theme data, and locale are usually tree-scoped, not global. Using context means different subtrees can have different values (useful for testing, previews, and multi-tenant apps).
- Mixing too many patterns: If you have
$state, a store, and context all managing related state, the system is harder to understand than it needs to be. Pick one approach for each piece of state and commit to it.
Key Takeaways
$statefor local component state. This covers the majority of cases.- Props for passing data down to children. Callbacks and
$bindablefor sending data back up. - Context for sharing data within a component subtree without prop drilling. Per-tree, not global.
- Stores for truly global state that must be accessible from non-component code or disconnected parts of the app.
- URL parameters for state that should be shareable, bookmarkable, and navigable.
- Start with the simplest tool that works. Escalate only when you hit a proven limitation. Most apps need less state management infrastructure than developers expect.