Skip to content

Core Reference

@zhuangtai-js/core provides framework-agnostic state primitives. It has no third-party runtime dependencies and no hidden scheduling.

Terminal window
pnpm add @zhuangtai-js/core

Create readable, writable, watchable state.

import { atom } from "@zhuangtai-js/core";
const count = atom(0);
count.get();
count.set(1);
count.set((value) => value + 1);
count.watch((value, prevValue) => {});

Return the current value.

const value = count.get();

Update the value immediately. nextValue can be a value or an updater function.

count.set(1);
count.set((value) => value + 1);

Function values are not supported as atom values: set(fn) treats fn as an updater, and passing a function type when defining an atom is a type error. To store a function, wrap it in an object, e.g. atom({ fn }).

const fnAtom = atom({ fn: () => {} });
const nextFn = () => {};
fnAtom.set({ fn: nextFn });

Register a synchronous watcher and call it once immediately with the current value. The return value stops the watcher.

const stop = count.watch((value, prevValue) => {
console.log(value, prevValue);
});
stop();

Calling set() on an atom while that same atom is notifying watchers throws. Watchers may update other atoms, but avoid cycles.

Derive read-only state from one or more atoms, with dependencies discovered automatically from the .get() calls made inside the derive.

import { atom, computed } from "@zhuangtai-js/core";
const count = atom(1);
const double = computed(() => count.get() * 2);
double.get(); // 2
const firstName = atom("Ada");
const lastName = atom("Lovelace");
const fullName = computed(() => `${firstName.get()} ${lastName.get()}`);

computed() calculates its initial value when created. It subscribes to sources only while watched, and get() recalculates from current source values. computed() discovers dependencies from the .get() calls it makes during the derive. The subscription set comes from the actual reads, so declared sources can’t drift away from what the derive really uses. Conditional dependencies switch automatically. A shape like computed(() => flag.get() ? a.get() : b.get()) unsubscribes the old branch and subscribes the new one when flag flips. Tracking only happens inside the synchronous derive. Reads after await or inside setTimeout are not tracked, so derives should stay synchronous. Nested computed values stay isolated. Reading inner.get() inside an outer derive makes the outer depend on inner itself, not on inner’s internal sources.

Create an atom creator that can install plugins. The default atom() export stays unextended.

import { createAtom } from "@zhuangtai-js/core";
import { persist } from "@zhuangtai-js/persist";
const atom = createAtom().use(persist);

Plugins are installed on creators, not atom instances.

@zhuangtai-js/core exports common public types:

  • Atom<Value>
  • Computed<Value>
  • ReadableAtom<Value>
  • NextValue<Value>
  • Watcher<Value>
  • StopWatch
  • AtomValue<Atom>
  • AtomCreator
  • AtomCreatorPlugin
  • AtomCreatorPluginContext

Internal creator argument types are not exported from the package entrypoint.

  • set() applies immediately.
  • watch() runs synchronously and is called once when registered (prevValue is an undefined sentinel). For an Atom<T | undefined> this cannot distinguish the first notification from a previous value that happened to be undefined.
  • Equality uses Object.is.
  • Object and array updates are reference-based; use immutable updates.
  • Watcher callbacks are isolated: a throwing watcher does not interrupt the current round; after all watchers run, a single error is rethrown as-is and multiple errors are rethrown in an AggregateError.
  • A watcher added during notification is immediately invoked once with (currentValue, undefined), but does not join the broadcast snapshot in progress.
  • A multi-source computed is a synchronous snapshot, not a transactional consistency boundary: updating several sources one by one, or updating other sources from within a watcher, can expose intermediate combinations; keep tightly coupled values in the same atom.
  • computed compares derived results with Object.is; a derive that returns a new object/array every time is treated as changed and may notify repeatedly, so return a reference-stable value when you need to suppress notifications.
  • Plugins are idempotent by id: installing a plugin with the same id via use() is a no-op, and plugin ids must be globally unique.
  • The core does not add hidden batching, deferring, debouncing, or transactions.