---
title: State Management
sort: 120
section-id: core-concepts
keywords: state, signals, store, reactive, computed, effect, state management
description: Velox's reactive state management system — signals, stores, and reactive primitives
language: en
---
# State Management
Velox uses a signals-based reactivity system for client-side state. Signals are fine-grained reactive primitives — when a signal's value changes, only the DOM nodes that actually read that signal update. There is no virtual DOM diffing, no component tree re-render, and no unnecessary reconciliation.
## Signals
A signal is a reactive value container:
```typescript
import { signal } from 'velox/client';
const count = signal(0);
// Read the current value
console.log(count.value); // 0
// Update the value
count.value = 1;
console.log(count.value); // 1
```
In JSX, signals are automatically unwrapped — you do not need `.value` in templates:
```tsx
const count = signal(0);
// Both are equivalent:
{count.value}
{count}
```
The second form (`{count}` without `.value`) subscribes the DOM node directly to the signal. This is more efficient because Velox skips the component function entirely and updates only the text node when the signal changes.
## Computed Signals
A computed signal derives its value from one or more other signals. It re-evaluates automatically when its dependencies change:
```typescript
import { signal, computed } from 'velox/client';
const firstName = signal('Jane');
const lastName = signal('Doe');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
console.log(fullName.value); // "Jane Doe"
firstName.value = 'John';
console.log(fullName.value); // "John Doe"
```
Computed signals are lazy and memoised — the computation only runs when the value is read, and only re-runs if a dependency has changed since the last read.
## Effects
An effect runs a side effect whenever its reactive dependencies change:
```typescript
import { signal, effect } from 'velox/client';
const query = signal('');
effect(() => {
console.log('Search query changed:', query.value);
// this re-runs every time query.value changes
});
```
Effects clean themselves up automatically when the component unmounts. To clean up manually within an effect (e.g., cancel a previous request):
```typescript
effect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query.value}`, { signal: controller.signal })
.then(r => r.json())
.then(results => resultsSignal.value = results);
return () => controller.abort(); // cleanup function
});
```
## Stores
For shared state across components, define a store. A store is a module that encapsulates signals and exposes a clean interface:
```typescript
// lib/stores/cart.ts
import { signal, computed } from 'velox/client';
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
const items = signal([]);
export const cartStore = {
items,
total: computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
),
itemCount: computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
),
addItem(item: Omit) {
const existing = items.value.find(i => i.id === item.id);
if (existing) {
items.value = items.value.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
} else {
items.value = [...items.value, { ...item, quantity: 1 }];
}
},
removeItem(id: string) {
items.value = items.value.filter(i => i.id !== id);
},
clear() {
items.value = [];
},
};
```
Use the store in any component — signals are module-level singletons:
```tsx
import { cartStore } from '$lib/stores/cart';
export default function CartIcon() {
return (
);
}
```
## Batch Updates
Multiple signal updates within a `batch()` call are processed as a single reactive update, preventing intermediate renders:
```typescript
import { signal, batch } from 'velox/client';
const x = signal(0);
const y = signal(0);
const z = signal(0);
// Without batch: triggers 3 re-renders
x.value = 1;
y.value = 2;
z.value = 3;
// With batch: triggers 1 re-render
batch(() => {
x.value = 1;
y.value = 2;
z.value = 3;
});
```
## Async Signals
For async data that needs to integrate with the reactivity system, use `asyncSignal`:
```typescript
import { signal, asyncSignal } from 'velox/client';
const userId = signal(null);
const userProfile = asyncSignal(async () => {
if (!userId.value) return null;
const res = await fetch(`/api/users/${userId.value}`);
return res.json();
});
// userProfile.value is: { status: 'idle' | 'loading' | 'success' | 'error', data, error }
```
In JSX:
```tsx
```
## Server State with `@velox/query`
For server state (data fetched from APIs), the `@velox/query` package provides a higher-level solution with caching, revalidation, and optimistic updates:
```typescript
import { createQuery, createMutation } from '@velox/query';
const postsQuery = createQuery({
key: ['posts'],
fetcher: () => fetch('/api/posts').then(r => r.json()),
staleTime: 60_000, // data is fresh for 60 seconds
});
const createPostMutation = createMutation({
mutator: (data) => fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) }),
onSuccess: () => postsQuery.invalidate(),
});
```
## Persisting State
To persist signal state to `localStorage` or `sessionStorage`:
```typescript
import { persistedSignal } from 'velox/client';
const theme = persistedSignal('theme', 'light', {
storage: 'localStorage',
});
```
The signal is initialised from storage on mount and written back whenever it changes.
## Reactive Context (Dependency Injection)
For providing state to deeply nested component trees without prop drilling, use the context API:
```typescript
import { createContext, useContext } from 'velox/client';
const ThemeContext = createContext<'light' | 'dark'>('light');
// Provider (wraps a subtree)
// Consumer (anywhere in the tree below)
function ThemedButton() {
const theme = useContext(ThemeContext);
return ;
}
```