<!-- Skal docs — markdown mirror of https://skal.run/docs/state.html -->

# State & the Store

Ephemeral state is just Solid. Persistent state is `createSkalStore` — a reactive deep-object store backed by a log-structured native engine, hydrated before your first render.

## Ephemeral: plain Solid

```jsx
const [query, setQuery] = createSignal('');
const results = createMemo(() => search(query()));
```

Signals, memos, effects, resources — everything from `solid-js` works untouched. There is no bridge-specific state API to learn for in-memory state.

## Persistent: `createSkalStore`

```jsx
import { createSkalStore } from 'skal/store';

export const db = createSkalStore({
  name: 'store',
  version: 1,
  initial: {
    counter: 0,
    settings: { theme: 'dark', locale: 'en' },
    todos: [],                 // arrays are stable-id collections
    scratch: { note: '' },
  },
  config: {
    scratch: { persist: false },   // memory-only subtree
    todos:   { lazy: true },        // hydrate on first read, LRU-evicted
  },
});
```

Reads and writes are direct object access — the store is a deep reactive proxy, so any widget reading `db.counter` re-renders when it changes:

```jsx
<Text label={`Count: ${db.counter}`} />
<Button label="+1" onClick={() => db.counter++} />

db.settings.theme = 'light';      // persisted on write — one delta frame
db.todos.push({ text: 'ship it' }); // collection ops persist per element
```

## The native engine

Persistence is not JSON-to-disk. Writes append per-leaf delta frames to a log-structured store implemented inside libskal (segments, compaction, a keydir index) — a write is sub-millisecond and never blocks a frame. On launch, the engine pre-warms on a background thread while your bundle evaluates, so hydration is already done when the first render reads the store.

| Property | Behavior |
| --- | --- |
| Write | persist-on-write, per-leaf delta frames, batched fsync |
| Read | in-memory reactive proxy — native only on hydrate |
| Arrays | stable-id collections — push/splice persist element ops, not the array |
| Big values | auto-blob: large subtrees serialize as one frame instead of leaf recursion |
| Migration | `version` + `migrate(old)` hook runs before hydration completes |
| Web | same API, in-memory backend (persistence lands with the IndexedDB backend) |

## Schema versioning

```jsx
createSkalStore({
  name: 'store', version: 2,
  initial: { ... },
  migrate(old, fromVersion) {
    if (fromVersion < 2) old.settings.locale ??= 'en';
    return old;
  },
});
```

## The control handle

Data lives on the proxy; _operations on the store itself_ live behind the `STORE` symbol — deliberately off to the side so they can never collide with your keys:

```jsx
import { createSkalStore, STORE } from 'skal/store';

const ctl = db[STORE];
```

| Method | Returns |
| --- | --- |
| `ctl.backendKind()` | `'native'` on device · `'memory'` on web/tests — branch UI on it if you must |
| `ctl.version()` | the schema version currently on disk |
| `ctl.pending()` | writes staged but not yet flushed (watch it settle to 0) |
| `ctl.flushes()` | flush count this session — a cheap write-amplification gauge |
| `ctl.engineStats()` | `{ records, segments }` from the native engine |
| `ctl.initTiming()` | `{ total, open, migrate, hydrate, records }` in ms — how boot was spent |

The kitchen-sink's Store tab renders all of these live — it's the fastest way to build intuition for what a write actually costs.

## Recipes

### Collections with `<For>`

```jsx
import { For } from 'solid-js';

<Column gap={8}>
  <For each={db.todos}>
    {(todo) => (
      <ListTile title={todo.text}
        onTap={() => todo.done = !todo.done} />   // mutate the element directly
    )}
  </For>
</Column>

db.todos.push({ text: 'ship it', done: false }); // one element frame persisted
db.todos.splice(i, 1);                            // deletion persisted by stable id
```

Elements have stable identities, so `<For>` reconciles moves without re-rendering rows, and each element persists independently — pushing item 10,001 doesn't rewrite the other 10,000.

### Derived values

```jsx
const remaining = createMemo(() => db.todos.filter(t => !t.done).length);
<Text label={`${remaining()} left`} />
```

Memos subscribe with the same fine granularity as widgets — derive freely, persist only source data.

### Drafts that shouldn't outlive a crash

```jsx
config: { scratch: { persist: false } }   // in createSkalStore

db.scratch.note = draft();                // reactive, shared, never on disk
```

### Resetting a subtree

```jsx
db.todos.splice(0);                       // clear a collection
Object.assign(db.settings, { theme: 'dark', locale: 'en' }); // reset leaves
```

## Guarantees & limits

-   **Crash safety:** the engine is log-structured — a write appends; recovery replays the log. A crash can lose at most the last unflushed batch (`ctl.pending()`), never corrupt what's on disk.
-   **Compaction is automatic.** Segments fold in the background; you never vacuum.
-   **Big values are fine, big _blobs_ aren't.** Large subtrees auto-serialize as single frames, but the store is for app state — put images and downloads in files and store the path.
-   **It's a store, not a database.** No queries, no indexes, no joins — reads are plain object access at memory speed. If you need SQL, wrap a pub.dev database and keep the store for state.

## What survives what

| State kind | Hot reload | App restart |
| --- | --- | --- |
| `createSignal` | resets | resets |
| `createHotState` | **survives** | resets |
| Router stack | **survives** | resets |
| `createSkalStore` | **survives** | **survives** |

[Previous← Components](components.html) [NextWrapping pub.dev packages →](native.html)
