StunkStunk

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 1

Action: 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.


historyreset() 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 too

Action: If you relied on history persisting after reset(), use clearHistory() and reset() separately.


persistclearStorage() added, onError on type mismatch

persist now returns a PersistedChunk with clearStorage():

const persisted = persist(theme, { key: "theme" });
persisted.clearStorage(); // removes key from storage

onError 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.

RemovedReplace with
useDeriveDerive outside component + useChunkValue
useComputedCompute outside component + useChunkValue
useChunkPropertyuseChunkValue(chunk, s => s.key)
useChunkValuesIndividual 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/combineAsyncChunks imports to stunk/query
  • Migrate computed([deps], fn)computed(() => fn) with .get() calls
  • Wrap middleware in { middleware: [...] }, change loggerlogger()
  • Rename withHistoryhistory, withPersistencepersist
  • Replace useDerive, useComputed, useChunkProperty, useChunkValues with useChunkValue
  • 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).

On this page