Migration Guide
Step-by-step guide for migrating between Stunk major versions.
v2 → v3
v3 is currently in alpha. Most changes can be applied incrementally right now — you don't need to wait for stable.
The v2 API is still fully supported. Migration is opt-in and can be done gradually. None of these changes break your existing code until you switch.
asyncChunk — import path changed
In v3, asyncChunk, infiniteAsyncChunk, and combineAsyncChunks moved to stunk/query:
// v2
import { asyncChunk } from "stunk";
// v3
import { asyncChunk } from "stunk/query";Action: Update all async imports to stunk/query.
computed() — no more dependency arrays
v3 auto-tracks dependencies via .get() calls — no array needed:
// v2
const total = computed([price, quantity], (p, q) => p * q);
// v3
const total = computed(() => price.get() * quantity.get());Use .peek() inside the function to read a value without tracking it as a dependency.
Action: Replace computed([deps], fn) with computed(() => fn) using .get() calls inside.
null is now a valid chunk value
In v2, null threw an error as an initial value. In v3 it's valid — undefined is the only forbidden value:
// v2 — threw
chunk(null); // ❌
// v3 — valid
chunk<string | null>(null); // ✅Action: No change needed if you weren't using null. If you worked around the v2 limitation, you can simplify now.
subscribe() no longer fires immediately
In v2, subscribing to a chunk immediately called the callback with the current value. In v3 it doesn't — the callback only fires on changes:
// v2 — fired immediately
count.subscribe((v) => console.log(v)); // logged 0
// v3 — only fires on change
count.subscribe((v) => console.log(v)); // nothing logged
count.set(1); // logged 1Action: If you relied on the immediate call, read chunk.get() directly alongside your subscribe().
Middleware renamed
withHistory and withPersistence are renamed in v3:
// v2
import { withHistory, withPersistence } from "stunk/middleware";
const tracked = withHistory(count);
const persisted = withPersistence(theme, { key: "theme" });
// v3
import { history, persist } from "stunk/middleware";
const tracked = history(count);
const persisted = persist(theme, { key: "theme" });Action: Update imports and rename usages.
Middleware config shape changed
Middleware is now passed inside a config object:
// v2
const count = chunk(0, [logger, nonNegativeValidator]);
// v3
const count = chunk(0, { middleware: [logger(), nonNegativeValidator] });Note logger() is now called as a factory — logger() not logger.
Action: Wrap middleware in { middleware: [...] } and add () to logger.
history — reset() now clears history
In v2, calling reset() on a history-wrapped chunk reset the value but left the history stack intact — canUndo() could still return true. In v3, reset() clears the stack:
tracked.set(1);
tracked.set(2);
tracked.reset();
tracked.get(); // initial value
tracked.canUndo(); // false — history cleared tooAction: If you relied on history persisting after reset(), use clearHistory() and reset() separately.
persist — clearStorage() added, onError on type mismatch
persist now returns a PersistedChunk with clearStorage():
const persisted = persist(theme, { key: "theme" });
persisted.clearStorage(); // removes key from storageonError is now also called when the persisted value has a different type than the initial chunk value — not just on serialization errors.
React hooks removed
useDerive, useComputed, useChunkProperty, and useChunkValues are removed in v3.
| Removed | Replace with |
|---|---|
useDerive | Derive outside component + useChunkValue |
useComputed | Compute outside component + useChunkValue |
useChunkProperty | useChunkValue(chunk, s => s.key) |
useChunkValues | Individual useChunkValue calls |
// v2
const doubled = useDerive(count, (n) => n * 2);
const total = useComputed([price, qty], (p, q) => p * q);
const name = useChunkProperty(user, "name");
// v3
const doubled = count.derive((n) => n * 2); // outside component
const total = computed(() => price.get() * qty.get()); // outside component
function Component() {
const doubledValue = useChunkValue(doubled);
const totalValue = useChunkValue(total);
const name = useChunkValue(user, (u) => u.name);
}Action: Move all derive/compute calls outside components. Replace removed hooks with useChunkValue.
v3 React API — four hooks only
import {
useChunk, // read + write
useChunkValue, // read-only
useAsyncChunk, // async state
useInfiniteAsyncChunk, // infinite scroll
} from "stunk/react";Migration checklist
v2 → v3
- Move
asyncChunk/infiniteAsyncChunk/combineAsyncChunksimports tostunk/query - Migrate
computed([deps], fn)→computed(() => fn)with.get()calls - Wrap middleware in
{ middleware: [...] }, changelogger→logger() - Rename
withHistory→history,withPersistence→persist - Replace
useDerive,useComputed,useChunkProperty,useChunkValueswithuseChunkValue - Audit any code that relies on
subscribe()firing immediately on registration
v1 → v2
v1 is no longer supported. Upgrade to at least v2.8.1.
update() removed — use set() with an updater
// v1
count.update((n) => n + 1);
// v2+
count.set((n) => n + 1);Action: Replace every chunk.update(fn) call with chunk.set(fn).