npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fluidstate

v1.0.0

Published

Library for fine-grained reactivity state management

Readme

fluidstate

View interactive documentation on the official website.

fluidstate is a JavaScript library for fine-grained, signals-based reactive state management. It provides a powerful and flexible way to create applications where data changes automatically propagate through your system, ensuring that your UI or other side-effects are always synchronized with the application state.

import { createReactive, createReaction, runAction } from "fluidstate";

// Turn your reactive objects into reactive proxies
const businessState = createReactive({
	// Properties become reactive:
	itemsMade: 100,
	itemCost: 5,
	itemsSold: 80,
	itemPrice: 7,

	// Getters become computed reactive properties, cached until
	// their dependencies change:
	get totalCost() {
		return businessState.itemsMade * businessState.itemCost;
	},
	get totalRevenue() {
		return businessState.itemsSold * businessState.itemPrice;
	},
	get profit() {
		return businessState.totalRevenue - businessState.totalCost;
	},

	// Methods become batched actions that can mutate reactive data:
	setItemsSold(itemsSold) {
		businessState.itemsSold = itemsSold;
	},
});

createReaction(() => {
	console.log(`Total cost: $${businessState.totalCost}`);
});
// LOGS: Total cost: $500

createReaction(() => {
	console.log(`Total revenue: $${businessState.totalRevenue}`);
});
// LOGS: Total revenue: $560

createReaction(() => {
	console.log(`Total profit: $${businessState.profit}`);
});
// LOGS: Total profit: $60

// Use reactive methods (like `setItemsSold`) or `runAction` to mutate reactive data:
runAction(() => {
	businessState.itemsSold = 81;
});
// LOGS: Total revenue: $567
// LOGS: Total profit: $67
// (does not log or recalculate total cost)

Table of Contents

What is Signals-Based Reactivity?

Signals-based reactivity is a programming paradigm that simplifies state management by creating a declarative and automatic data flow. At its core, it involves:

  • Reactive Values (Signals/Atoms): Pieces of state that, when changed, can notify dependents.
  • Computed Values (Derived Signals): Values that are derived from other reactive values. They automatically update when their dependencies change and memoize their result.
  • Reactions (Effects): Functions that run in response to changes in reactive values they depend on. These are typically used for side effects, like updating the DOM.

When a reactive value changes, the system automatically identifies and re-executes only the specific computations and reactions that depend on it. This fine-grained approach leads to efficient updates and makes it easier to reason about data flow. Libraries like SolidJS and MobX are prominent examples of this paradigm. fluidstate aims to provide a similar level of power and ergonomics, acting as a versatile wrapper that utilizes a foundational reactive layer (such as MobX).

What fluidstate Provides

fluidstate offers a beautiful and ergonomic API to supercharge your dynamic applications by adding deep fine-grained reactivity to your objects, arrays, Sets, and Maps. Work with your data naturally by wrapping your objects in createReactive, seamlessly transforming them: properties become reactive values, getters become efficiently memoized computed values, and methods - automatically batched actions.

This comprehensive toolkit lets you:

  • Make your objects reactive: Use createReactive to transform your objects into reactive proxies.

  • React effortlessly to changes: Use createReaction to subscribe to changes in your reactive objects via automatic dependency tracking.

  • Handle async operations reactively: Use getResult to turn Promises into trackable reactive state.

  • Gain granular control: Create inert snapshots of reactive data with cloneInert, control scheduling of side-effects by providing your own scheduler, and customize the equality handling by specifying equals function.

  • Integrate complex systems: Synchronize state across distinct reactive systems using "Reactive Remotes".

  • Extend functionality: Hook into reactive object lifecycle with a powerful plugin system for logging, validation, versioning or synchronization.

fluidstate simplifies state management, helping you build responsive and efficient applications where data flow is clear and automatic.

Getting Started

Installation

You can install fluidstate and a reactive layer provider (e.g., fluidstate-mobx or fluidstate-alien) using npm or yarn:

npm install fluidstate fluidstate-mobx
# or
yarn add fluidstate fluidstate-mobx

Available Reactive Layers

fluidstate provides reactive functionality on top of a reactive layer of your choosing. The benefit of this approach is the flexibility to swap out one reactive layer for another at any point while keeping the convenience and ergonomics of the fluidstate reactivity API. You may choose one of the officially supported reactive layers, find an unofficial one, or create your own.

Official Reactive Layers:

Providing the Reactive Layer

Before using fluidstate, you must provide your chosen reactive layer by calling provideReactiveLayer. This is only done once at your application's entry point.

import { provideReactiveLayer } from "fluidstate";
import { getReactiveLayer } from "fluidstate-mobx"; // Or your chosen provider
// Note: for Preact Signals-based reactive layer, import as follows:
// import { getReactiveLayer } from "fluidstate-preact/reactive-layer";

// Get the reactive layer instance from the provider
const reactiveLayer = getReactiveLayer();

// Provide it to fluidstate
provideReactiveLayer(reactiveLayer);

// Now you can use fluidstate's API throughout your application:
// import { createReactive, createReaction } from "fluidstate";
// ...

Integration with UI Libraries

To bridge the gap between reactive state management and popular UI libraries, we aims to provide official tools to easily connect the fluidstate-based reactive state with UI components. At the moment, fluidstate provides official integrations with:

  • fluidstate-react: Provides essential hooks, higher-order components (HOCs), and other utilities to connect fluidstate to your React components.
  • fluidstate-preact: Offers a similar toolkit specifically designed for Preact, extended with efficient @preact/signals support, making it easy to build reactive and performant UIs.

These libraries handle the subscription and re-rendering logic, allowing your components to reactively update whenever the underlying fluidstate-based reactive data changes.

Core Concepts & Usage

Creating Reactive State: createReactive

The createReactive function is your primary tool for making data reactive. It deeply converts objects, arrays, Sets, and Maps.

import { createReactive, createReaction } from "fluidstate";

type UserProfile = {
	firstName: string;
	lastName: string;
	hobbies: string[];
	get fullName(): string;
	addHobby(hobby: string): void;
	updateName(firstName: string, lastName: string): void;
};

const user = createReactive<UserProfile>({
	firstName: "Jane",
	lastName: "Doe",
	hobbies: ["coding", "reading"],
	get fullName() {
		// This getter becomes a memoized computed value.
		// It only recalculates if firstName or lastName changes and it's being observed.
		console.log("Calculating fullName...");
		return `${this.firstName} ${this.lastName}`;
	},
	// Methods are automatically wrapped in actions.
	// Changes within them are batched, and reactions run only once after completion.
	addHobby(hobby: string) {
		this.hobbies.push(hobby);
	},
	updateName(firstName: string, lastName: string) {
		this.firstName = firstName; // Change 1
		this.lastName = lastName; // Change 2
		// Reactions depending on fullName or firstName/lastName will run once.
	},
});

createReaction(() => {
	console.log(`User: ${user.fullName}`);
});
// LOGS: Calculating fullName...
// LOGS: User: Jane Doe

createReaction(() => {
	console.log(`Hobbies: ${user.hobbies.join(", ")}`);
});
// LOGS: Hobbies: coding, reading

user.addHobby("hiking");
// LOGS: Hobbies: coding, reading, hiking
// (fullName reaction doesn't re-run as its dependencies didn't change)

user.updateName("John", "Smith");
// LOGS: Calculating fullName...
// LOGS: User: John Smith
// (addHobby reaction doesn't re-run)
  • Deep reactivity: Objects, arrays, maps and sets nested within a reactive structure also become reactive.
  • Getters become computed values: user.fullName is automatically a memoized computed value.
  • Methods become actions: user.addHobby and user.updateName are automatically wrapped in actions, batching their internal changes.

Reacting to Changes: createReaction

Reactions are side effects that run in response to state changes. createReaction automatically tracks dependencies accessed during its execution.

import { createReactive, createReaction, runAction } from "fluidstate";

const settings = createReactive({
	theme: "dark",
	fontSize: 14,
});

// This reaction runs immediately and whenever theme or fontSize changes.
const settingsReaction = createReaction(() => {
	console.log(`Theme: ${settings.theme}, Font size: ${settings.fontSize}px`);
	// You can perform any side effect here, like updating UI.
});
// LOGS: Theme: dark, Font size: 14px

runAction(() => {
	settings.theme = "light";
});
// LOGS: Theme: light, Font size: 14px

// To stop a reaction and clean up its observers:
settingsReaction.stop();

runAction(() => {
	settings.fontSize = 16; // This change is no longer observed by settingsReaction
});
// No new logs from settingsReaction.

A reaction can specify one or more cleanup functions via createCleanup.

Important Note: Every reaction created MUST be explicitly stopped using its stop() method when it is no longer needed. Failure to do so can lead to memory leaks, especially if the reactive data the reaction depends on is also not garbage collected. Reactions hold references to their dependencies, and if a reaction is not stopped, it may prevent those dependencies (and potentially large parts of your application state) from being cleaned up by the garbage collector.

Computed Values (via Getters)

As seen in the createReactive example, getters on reactive objects automatically become memoized computed values. They efficiently recalculate only when their underlying reactive dependencies change and they are being observed by a reaction or another computed value.

const stats = createReactive({
	valueA: 10,
	valueB: 20,
	get sum() {
		console.log("Recalculating sum...");
		return this.valueA + this.valueB;
	},
});

createReaction(() => {
	console.log(`Sum is: ${stats.sum}`);
});
// LOGS: Recalculating sum...
// LOGS: Sum is: 30

runAction(() => {
	stats.valueA = 15;
});
// LOGS: Recalculating sum...
// LOGS: Sum is: 35

console.log(stats.sum); // Accessing outside an active reaction
// LOGS: Sum is: 35 (No "Recalculating sum..." because it's memoized and dependencies haven't changed since last calculation by an active reaction)

runAction(() => {
	stats.valueB = 25;
});
// If the reaction is still active:
// LOGS: Recalculating sum...
// LOGS: Sum is: 40

// If the reaction observing `stats.sum` were stopped (or if `stats.sum` wasn't observed by any active reaction),
// `stats.sum` would still be accessible. It would then recalculate its value lazily (only upon access)
// if its dependencies (`valueA` or `valueB`) had changed since its last computation.

For standalone computed values not tied to an object's getter, see createComputedAtom in the advanced API.

Actions and Transactions

To ensure atomicity and prevent reactions from running multiple times during a sequence of changes, use runAction or runTransaction. Methods on reactive objects created with createReactive are automatically wrapped in actions.

import { createReactive, createReaction, runAction } from "fluidstate";

const counter = createReactive({ value: 0 });

createReaction(() => {
	console.log(`Counter value: ${counter.value}`);
});
// LOGS: Counter value: 0

// Without action, reaction would run for each increment if they were separate statements:
// counter.value++; // Reaction runs
// counter.value++; // Reaction runs again

// With runAction, all changes are batched, and the reaction runs once.
runAction(() => {
	counter.value++; // Change 1
	counter.value++; // Change 2
	console.log("Inside action, all changes batched.");
});
// LOGS: Inside action, all changes batched.
// LOGS: Counter value: 2 (reaction runs once after action completes)

runTransaction is similar to runAction but is typically used by library internals or for more complex batching scenarios. runAction is the preferred API for most user-level batching.

API Documentation

Core Reactive Primitives & Creation

These are fundamental for creating and managing reactive state.

createReactive(initialValue, options?)

Takes a plain JavaScript object, array, Set, Map, or Promise and returns a deeply reactive version.

  • initialValue: T: The initial value to make reactive.
  • options?: ReactiveOptions: Configuration options.
    • deep?: boolean: (Default: true) Whether to make the object deeply reactive. If false, only direct properties/elements are reactive, not nested structures.
    • reactiveList?: (string | symbol)[]: If provided, only these properties will be reactive.
    • nonReactiveList?: (string | symbol)[]: If provided, these properties will not be reactive.
    • plugins?: ReactivePlugin[]: An array of plugins to apply. See Plugin system.
    • name?: string: A debug name for the reactive object.
const user = createReactive({ name: "Alice", age: 30 }); // Reactive object
const items = createReactive([1, 2, 3]); // Reactive array
const dataSet = createReactive(new Set([10, 20])); // Reactive Set
const dataMap = createReactive(new Map([["key", "value"]])); // Reactive Map
const promiseHolder = createReactive({ data: fetch("/api/data") }); // Wrapping Promise

createReaction(effect, options?)

Creates a reaction that automatically tracks its dependencies and re-runs when they change. It returns a reaction object which has a stop() method that must be eventually called to dispose of the reaction.

  • effect: () => void: The function to run as a reaction. While running, it tracks access to any reactive properties read inside and will re-run (or be re-scheduled) when any of those reactive properties change. The function can optionally create any cleanups via createCleanup that will be executed before the next run or when the reaction is stopped.

  • options?: object:

    • scheduler?: (fn) => void: Custom scheduler to control when the reaction runs. If provided, the reaction might not run immediately. The default scheduler runs the reaction synchronously.
const data = createReactive({ value: 0 });

const reaction = createReaction(() => {
	console.log(`Data value: ${data.value}`);
});
// LOGS: Data value: 0

runAction(() => {
	data.value = 1;
});
// LOGS: Data value: 1

reaction.stop();

Important Note: Every reaction created MUST be explicitly stopped using its stop() method when it is no longer needed. Failure to do so can lead to memory leaks, especially if the reactive data the reaction depends on is also not garbage collected. Reactions hold references to their dependencies, and if a reaction is not stopped, it may prevent those dependencies (and potentially large parts of your application state) from being cleaned up by the garbage collector.

createCleanup(cleanupFn)

A utility to be used within a createReaction's effect function to register cleanup logic. The cleanupFn is called when the reaction is stopped or before it re-runs. This is helpful for managing multiple distinct cleanup operations within a single reaction effect.

import {
	createReactive,
	createReaction,
	createCleanup,
	runAction,
} from "fluidstate";

const appState = createReactive({
	button: document.createElement("button"),
	isActive: true,
});

const reaction = createReaction(() => {
	if (!appState.isActive) {
		console.log("Feature is inactive");

		createCleanup(() => {
			console.log("Cleanup after feature becoming inactive");
		});
		return;
	}

	const handleButtonClick = () => {
		console.log(`Element clicked!`);
	};

	const button = appState.button;
	button.addEventListener("click", handleButtonClick);
	console.log(`Added click listener to button`);
	createCleanup(() => {
		button.removeEventListener("click", handleButtonClick);
		console.log(`Removed click listener from button`);
	});
});

// Initial run
// LOGS: Added click listener to button

// Simulate changing state that causes re-run and cleanup
runAction(() => {
	appState.button = document.createElement("div"); // This will trigger a re-run
});
// LOGS: Removed click listener from button

// Simulate deactivation
runAction(() => {
	appState.isActive = false; // This will trigger a re-run
});
// LOGS: Removed click listener from button
// LOGS: Feature is inactive

// Finally, stop the reaction explicitly
reaction.stop();
// LOGS: Cleanup after feature becoming inactive

untrack(fn)

Executes the function fn without tracking any reactive dependencies accessed within it. Any reactive values read inside fn will not cause the outer reaction or computed value to re-evaluate if those specific untracked values change.

const state = createReactive({ trackedValue: 1, untrackedInfo: 100 });

createReaction(() => {
	const info = untrack(() => {
		// state.untrackedInfo is accessed here but won't be a dependency of the reaction
		return state.untrackedInfo * 2;
	});
	console.log(`Tracked: ${state.trackedValue}, Info: ${info}`);
});
// LOGS: Tracked: 1, Info: 200

runAction(() => {
	state.trackedValue = 2;
});
// LOGS: Tracked: 2, Info: 200 (reaction re-runs due to state.trackedValue)

runAction(() => {
	state.untrackedInfo = 150;
});
// (no log: Reaction does NOT re-run because state.untrackedInfo was accessed in untrack())

This is particularly useful when a reaction needs to modify a reactive value and it is important for the reaction not to depend on it. If a reaction directly modifies a value it reads, it can create a cycle where the modification triggers the reaction, which then modifies the value again, leading to an infinite loop or unexpected behavior. By wrapping the reading of the modification path in untrack, you can prevent the reaction from subscribing to changes in that specific part of the state, while still allowing it to react to other dependencies.

const state = createReactive({
	object: {
		nested: 100,
	},
	latestValue: 200,
});

createReaction(() => {
	// The reaction becomes dependent on `state.latestValue` but not
	// on `state.object.nested` because it `state.object` is accessed
	// in `untrack` and because property mutations such as `.nested = ...`
	// do not track property access
	untrack(() => state.object).nested = state.latestValue;
});

State Inspection & Manipulation

isReactive(value)

Returns true if the given value is a reactive object created by fluidstate, false otherwise.

const plain = {};
const reactiveObj = createReactive({});
console.log(isReactive(plain)); // false
console.log(isReactive(reactiveObj)); // true

getInert(reactiveValue)

If reactiveValue is a reactive proxy, returns its underlying inert (plain JavaScript) target. Otherwise, returns the value as is. This is a shallow operation. For a deep inert copy, see cloneInert.

const reactiveUser = createReactive({ name: "Bob" });
const inertUser = getInert(reactiveUser);

console.log(isReactive(reactiveUser)); // true
console.log(isReactive(inertUser)); // false
console.log(inertUser.name); // "Bob"

ensureInert(value)

Ensures the returned value is not reactive. If value is reactive, it returns its inert target (similar to getInert). Otherwise, returns value itself. Useful when you need to store a value that might be reactive but you want to store its plain form.

const original = { id: 1 };
const reactiveCopy = createReactive(original);
const itemToStore = ensureInert(reactiveCopy); // itemToStore is { id: 1 } (plain object, same as original)
const plainItemToStore = ensureInert({ id: 2 }); // plainItemToStore is { id: 2 }

getReactive(inertValue)

If inertValue is the inert target of an existing reactive proxy, returns that proxy. Otherwise, returns null.

const inert = { id: 1 };
const reactiveProxy = createReactive(inert);

const foundReactive = getReactive(inert);
console.log(foundReactive === reactiveProxy); // true

const anotherInert = { id: 2 };
console.log(getReactive(anotherInert)); // null

getComputedKeys(reactiveObject)

Returns an array of keys (strings or symbols) that are getters (and thus, computed properties) on the reactive object.

const store = createReactive({
	data: 123,
	get computedData() {
		return this.data * 2;
	},
	normalMethod() {},
});

console.log(getComputedKeys(store)); // ['computedData']

deepObserve(reactiveTarget)

When used inside a reaction's effect function, deepObserve(reactiveTarget) tells the reaction to subscribe to all changes within the reactiveTarget, no matter how deeply nested they are.

  • reactiveTarget: T: The reactive object, array, Set, or Map to observe deeply.
import {
	createReactive,
	createReaction,
	deepObserve,
	runAction,
} from "fluidstate";

const user = createReactive({
	name: "Carol",
	address: { city: "New York", zip: "10001" },
	hobbies: ["skiing", "coding"],
});

const deepReaction = createReaction(() => {
	// This makes the reaction sensitive to any change within the user object.
	deepObserve(user);
	untrack(() => {
		// Using untrack to prevent JSON.stringify from creating unintended dependencies
		console.log("User data changed:", JSON.stringify(user));
	});
});
// LOGS: User data changed: {"name":"Carol","address":{"city":"New York","zip":"10001"},"hobbies":["skiing","coding"]}

runAction(() => {
	user.name = "Charles";
});
// LOGS: User data changed: {"name":"Charles","address":{"city":"New York","zip":"10001"},"hobbies":["skiing","coding"]}

runAction(() => {
	user.address.city = "London";
});
// LOGS: User data changed: {"name":"Charles","address":{"city":"London","zip":"10001"},"hobbies":["skiing","coding"]}

runAction(() => {
	user.hobbies.push("swimming");
});
// LOGS: User data changed: {"name":"Charles","address":{"city":"London","zip":"10001"},"hobbies":["skiing","coding","swimming"]}

deepReaction.stop(); // Stop observing

cloneInert(reactiveSource, options?)

Creates a non-reactive (inert) clone of a reactive object, array, Set, or Map. By default, the clone is deep.

  • reactiveSource: T: The reactive data structure to clone.
  • options?: CloneInertOptions:
    • deep?: boolean: (Default: true) Whether to perform a deep clone. If false, it's a shallow clone (nested reactive objects/arrays will remain reactive proxies in the clone).
    • excludeComputed?: boolean: (Default: false) Whether computed property values (getters) should be excluded from the clone. If true, the property will not exist on the cloned object. If false (default), the computed value at the time of cloning is included as a static value.

Returns CloneInertResult<T>, which is the cloned, inert version of reactiveSource.

const state = createReactive({
	user: { name: "Dave", settings: { theme: "dark" } },
	posts: [{ id: 1, title: "First Post" }],
	get upperName() {
		return this.user.name.toUpperCase();
	},
});

// Deep clone including computed values
const inertSnapshot = cloneInert(state);
console.log(isReactive(inertSnapshot.user)); // false
console.log(isReactive(inertSnapshot.posts[0])); // false
console.log(inertSnapshot.upperName); // "DAVE"
inertSnapshot.user.name = "David"; // This doesn't affect the original reactive 'state'
console.log(state.user.name); // "Dave"

// Deep clone excluding computed values
const inertSnapshotNoComputed = cloneInert(state, { excludeComputed: true });
console.log("upperName" in inertSnapshotNoComputed); // false

Actions & Transactions

These ensure that multiple state changes trigger reactions only once, after all changes are complete.

createAction(fn)

Wraps a function fn into an action. When the wrapped function is called, all state modifications within it are batched. Methods on objects created with createReactive are automatically wrapped like this.

  • fn: (...args: any[]) => T: The function to wrap.
const counter = createReactive({ value: 0 });
createReaction(() => console.log(counter.value)); // LOGS: 0

const incrementTwiceBy = createAction((amount: number) => {
	counter.value += amount;
	counter.value += amount; // Another modification
});

incrementTwiceBy(2);
// LOGS: 4

runAction(fn)

Executes the function fn within an action context, batching all state changes made directly within fn.

  • fn: () => T: The function to execute.
const counter = createReactive({ value: 0 }); // Assuming counter from previous example or re-declared
createReaction(() => console.log(`Counter: ${counter.value}`)); // LOGS: Counter: 0

runAction(() => {
	counter.value = 5;
	counter.value = 10;
}); // Reaction depending on counter.value runs once with value 10.
// LOGS: Counter: 10

runTransaction(fn)

Similar to runAction, but typically used for lower-level or more complex batching scenarios. For most application code, runAction is preferred.

  • fn: () => T: The function to execute.

Reactive Options Configuration

You can configure default options globally for how fluidstate handles equality checks (which affects when reactions re-run or computed values re-calculate) and reaction scheduling.

CHANGED (Symbol)

fluidstate exports a special symbol CHANGED. You can assign this symbol as the new value to a reactive property or variable to force propagation of a change, effectively bypassing the standard equality check. This is useful when the specific new value itself is not important, but you need to signal that a change occurred.

import { createReactive, createReaction, CHANGED, runAction } from "fluidstate";

const state = createReactive({ data: CHANGED });
let changeCount = 0;

createReaction(() => {
	// Access state.data.value to establish dependency
	const currentValue = state.data;
	console.log(`Data changed. Change count: ${changeCount++}`);
});
// LOGS: Data changed. Change count: 0

runAction(() => {
	// Signaling a change by re-assigning the value to `CHANGED`
	state.data = CHANGED;
});
// LOGS: Data changed. Change count: 1

runAction(() => {
	// Signaling a change again
	state.data = CHANGED;
});
// LOGS: Data changed. Change count: 2

The default equals function (used by reactive values and computed atoms) will consider a value unequal to its previous one if the new value is CHANGED. If you provide a custom equals function, you should typically handle CHANGED as well, usually by returning false (not equal) if newValue === CHANGED. (See example under configureDefaultComputedOptions).

configureDefaultReactiveValueOptions(options)

Sets default options for Atoms (these underlie properties in createReactive).

  • options: { equals?: (a: any, b: any) => boolean }

getDefaultReactiveValueOptions()

Gets the current default options for Atoms.

configureDefaultComputedOptions(options)

Sets default options for ComputedAtoms (these underlie getters in createReactive and values from createComputedAtom).

  • options: { equals?: (a: any, b: any) => boolean }

getDefaultComputedOptions()

Gets the current default options for ComputedAtoms.

configureDefaultReactionOptions(options)

Sets default options for Reactions (used by createReaction).

  • options: { scheduler?: (callback: () => void) => void } (see createReaction for more context).

getDefaultReactionOptions()

Gets the current default options for Reactions.

Example of configuration

import {
	configureDefaultComputedOptions,
	createReactive,
	createReaction,
	runAction,
	CHANGED,
} from "fluidstate";

// Custom equals for computed properties: consider numbers close enough if difference is small
const fuzzyEquals = (oldValue: unknown, newValue: unknown) => {
	if (newValue === CHANGED) {
		// Essential for compatibility with CHANGED symbol
		return false;
	}
	if (typeof oldValue === "number" && typeof newValue === "number") {
		return Math.abs(oldValue - newValue) < 0.001;
	}
	return Object.is(oldValue, newValue);
};

configureDefaultComputedOptions({
	equals: fuzzyEquals,
});

const store = createReactive({
	x: 1.0,
	y: 2.0,
	get sum() {
		console.log("Calculating sum...");
		return this.x + this.y;
	},
});

createReaction(() => {
	console.log(`Sum: ${store.sum}`);
});
// LOGS: Calculating sum...
// LOGS: Sum: 3

runAction(() => {
	store.x = 1.0001; // This change is small enough according to fuzzyEquals
});
// LOGS: Calculating sum...
// (does not log "Sum: ..." because fuzzyEquals considers 3 and 3.0001 equal)

runAction(() => {
	store.x = 1.01; // This change is significant
});
// LOGS: Calculating sum...
// LOGS: Sum: 3.01

Advanced Primitives & Utilities

These are lower-level primitives or utilities, often used for more fine-grained control or by library authors.

createAtom(name?, options?)

Creates a basic reactive unit (an Atom). Atoms don't hold values themselves but are used to signal observation (reportObserved()) and changes (reportChanged()). This is typically used for building custom reactive data structures or integrating with non-Proxy-based state.

  • name?: string: A debug name for the atom.
  • options?: Atom-specific options (e.g., onBecomeObservedListener, onBecomeUnobservedListener - the underlying reactive layer must support them, and they are passed through by fluidstate).
import { createAtom, createReaction, runAction } from "fluidstate";

const nameAtom = createAtom("customNameAtom");
let nameValue = "Initial";

createReaction(() => {
	nameAtom.reportObserved(); // Must report observation within a tracking context
	console.log(nameValue);
});
// LOGS: Initial

runAction(() => {
	nameValue = "Updated";
	nameAtom.reportChanged(); // Must report change to trigger dependents
});
// LOGS: Updated

createComputedAtom(name, calculate, options?)

Creates a standalone memoized, reactive value derived from other reactive sources. Getters on objects made with createReactive are a more common way to achieve computed values.

  • name: string: A debug name for the computed atom.
  • calculate: () => T: The function to calculate the value. Dependencies (other atoms or reactive properties) accessed within this function are automatically tracked.
  • options?: Computed-specific options:
    • onBecomeObservedListener?, onBecomeUnobservedListener? (the underlying reactive layer must support them).
    • equals?: (oldValue: T, newValue: T) => boolean: Custom equality check (see Reactive Options Configuration).
import {
	createReactive,
	createComputedAtom,
	createReaction,
	runAction,
} from "fluidstate";

const product = createReactive({ price: 100, taxRate: 0.07 });

const totalPrice = createComputedAtom(
	"totalPrice",
	() => {
		console.log("Calculating totalPrice...");
		return product.price * (1 + product.taxRate);
	},
	{ equals: (a, b) => Math.abs(a - b) < 0.01 } // Custom equals
);

createReaction(() => {
	console.log(`Total: $${totalPrice.get().toFixed(2)}`);
});
// LOGS: Calculating totalPrice...
// LOGS: Total: $107.00

runAction(() => {
	product.price = 200;
});
// LOGS: Calculating totalPrice...
// LOGS: Total: $214.00

isTracking()

Returns true if the current code execution is within a reactive tracking context (e.g., inside a createReaction's effect or a createComputedAtom's calculate function), false otherwise.

Advanced: Reactive Remotes & Instance Management

These APIs are for advanced use cases, particularly when integrating multiple reactive systems or customizing the reactive layer.

provideReactiveLayer(layer)

As shown in Getting Started, this function is used to supply fluidstate with its core reactive primitives (atoms, computeds, reactions).

  • layer: ReactiveLayer: An object conforming to the ReactiveLayer interface (which is a subset of ReactiveInstance interface), which defines methods like createAtom, createComputedAtom, createReaction, etc.

getReactiveInstance()

Returns the ReactiveInstance of fluidstate itself. This can be useful for integrating with other systems or for creating "Reactive Remotes".

addReactiveRemote(remoteInstance, options)

Integrates a "remote" ReactiveInstance with the current (local) ReactiveInstance. This allows reactions in the remote instance that depend on data from the local instance to have their execution scheduled by the local instance.

  • remoteInstance: ReactiveInstance: The reactive instance to add as a remote.
  • options?: { scheduler?: (fn: () => void) => void }:
    • scheduler: A function that the local instance will use to schedule the execution of reactions from the remote instance.

Returns a handle (ReactiveRemote) that can be used with removeReactiveRemote.

// --- In a hypothetical Game Engine (Local System) ---
import {
	getReactiveInstance,
	addReactiveRemote,
	createReactive,
	ReactiveInstance,
} from "fluidstate";

export const createGameEngine = () => {
	let remote: ReactiveInstance | null = null;
	const engineState = createReactive({ gameTime: 0 });

	const connectExternalUI = (uiReactiveInstance: ReactiveInstance) => {
		remote = addReactiveRemote(uiReactiveInstance, {
			scheduler: (fnToRun) => engineScheduler.scheduleToEndOfFrame(fnToRun),
		});
	};

	return {
		engineState,
		connectExternalUI,
		destroyEngine: () => {
			if (remote) {
				removeReactiveRemote(remote);
			}
		},
	};
};

// --- In an External UI System (Remote System) ---
import { getReactiveInstance, createReaction } from "fluidstate";
const uiReactiveInstance = getReactiveInstance();
gameEngine.connectExternalUI(uiReactiveInstance);

createReaction(() => {
	// This reaction reads from engineState (local to game engine)
	// Its execution will be scheduled by the game engine's scheduler
	updateGameTimeDisplay(gameEngine.engineState.gameTime);
});

removeReactiveRemote(remote)

Removes a previously added reactive remote.

  • remote: ReactiveRemote: The handle returned by addReactiveRemote.

Utilities for Extending Prototypes

These utilities are generally for advanced scenarios where you might need to augment the behavior of objects wrapped by createReactive.

addSimplePrototype(objectPrototype)

Registers a prototype object to be considered "simple". Objects whose prototype is in this list will be treated as candidates for deep reactivity by default. This allows custom classes or objects with specific prototypes to be automatically wrapped in reactive proxies when encountered within a larger reactive structure, assuming deep reactivity is enabled.

removeSimplePrototype(objectPrototype)

Unregisters a prototype object, so it's no longer considered "simple". This is the counterpart to addSimplePrototype. Objects whose prototype was previously registered will no longer be identified as "simple" by default by isSimpleObject after being removed.

Promises

fluidstate provides utilities to observe the state of Promises within its reactive system. When a Promise is wrapped by createReactive (e.g., as a property of a reactive object or directly), getResult can be used within reactions or computed values to react to its lifecycle.

PromiseStatus

An enum representing the state of a Promise:

  • PromiseStatus.Loading
  • PromiseStatus.Success
  • PromiseStatus.Error

getResult(promise)

Given a Promise (especially one managed within a reactive structure), returns a regular JS object describing the current status of the promise (Loading, Success, Error) and its resolved value or rejection reason. The function itself returns a regular, non-reactive JS object, but when used inside a reaction or a computed, that reaction or computed becomes subscribed to the promise and gets re-triggered when the promise resolves or throws.

  • promise: Promise<T>: The promise to inspect.

Returns: PromiseResult<T> containing the promise status and its result or rejection reason.

Example:

import {
	createReactive,
	createReaction,
	getResult,
	PromiseStatus,
	runAction,
} from "fluidstate";

const fetchData = async (id: number, succeed: boolean): Promise<string> => {
	console.log(`Fetching data for ID: ${id}...`);
	await new Promise((resolve) => setTimeout(resolve, 100)); // Simulate network delay
	if (succeed) {
		return `Data for ID ${id} fetched successfully!`;
	} else {
		throw new Error(`Failed to fetch data for ID ${id}.`);
	}
};

const store = createReactive({
	itemId: 1,
	simulateSuccess: true,
	// This getter returns a new promise whenever itemId or simulateSuccess changes.
	// createReactive makes the getter itself a computed value.
	get currentDataPromise() {
		return fetchData(store.itemId, store.simulateSuccess);
	},
});

createReaction(() => {
	// Accessing store.currentDataPromise makes the reaction depend on this computed getter.
	// `getResult` then subscribes the reaction to the result of the promise.
	const result = getResult(store.currentDataPromise);

	switch (result.status) {
		case PromiseStatus.Loading:
			console.log("Loading data...");
			break;
		case PromiseStatus.Success:
			console.log("Success:", result.result);
			break;
		case PromiseStatus.Error:
			console.error("Error:", String(result.error));
			break;
	}
});

// Initially:
// LOGS: Fetching data for ID: 1...
// LOGS: Loading data...
// After ~100ms:
// LOGS: Success: Data for ID 1 fetched successfully!

// Example of triggering a new fetch by changing dependencies of currentDataPromise getter
setTimeout(() => {
	runAction(() => {
		store.itemId = 2;
		store.simulateSuccess = false;
	});
	// This causes currentDataPromise getter to produce a new promise instance.
	// The reaction re-runs because the getter (a dependency) changed.
	// LOGS: Fetching data for ID: 2...
	// LOGS: Loading data... (reaction re-runs, getResult sees new promise as pending)
	// After ~100ms (for the new fetch):
	// LOGS: Error: Error: Failed to fetch data for ID 2.
}, 500);

Plugin system

The plugin system in fluidstate allows you to hook into the lifecycle of changes within reactive objects, arrays, maps, and sets. This enables a wide range of use cases, such as logging, validation, synchronization with external systems, or implementing undo/redo functionality.

Plugins are defined as objects that can have two optional methods: beforeChange and afterChange.

  • beforeChange(changes: ReactiveChange[]): This method is called just before a set of changes is applied to a reactive data structure. It receives an array of ReactiveChange objects detailing what is about to change. You could use this hook for validation, potentially throwing an error to prevent the change.

  • afterChange(changes: ReactiveChange[]): This method is called immediately after a set of changes has been successfully applied. It also receives an array of ReactiveChange objects, describing what has changed. This is typically used for side effects like logging, updating other systems, or triggering further reactive updates.

ReactiveChange

Each ReactiveChange object in the array provides detailed information about a specific modification. The structure of this object varies depending on the type of data structure being modified (object, array, map, or set) and the nature of the change (add, update, delete). For precise details on the properties available for each type of change, refer to the specific *Change type definitions (e.g., ObjectChange, ArrayChange, MapChange, SetChange) exported by fluidstate.

Example: Server Synchronization Plugin

The following is a simple plugin that sends changes to a server for synchronization. This plugin will use the afterChange hook to report modifications.

For this example, we assume that an external function syncChangesToServer(changes: ReactiveChange[]): Promise<void> handles the mapping of local and remote reactive objects, performs network communication and request queueing, and getUserProfile(): Promise<UserProfile> retrieves the initial user profile information from the server.

import {
	ReactivePlugin,
	ReactiveChange,
	createReactive,
	runAction,
} from "fluidstate";

// --- Hypothetical external functions ---
declare function syncChangesToServer(changes: ReactiveChange[]): Promise<void>;
declare function getUserProfile(): Promise<UserProfile>;

// --- Plugin Example ---

const ServerSyncPlugin: ReactivePlugin = {
	afterChange: (changes: ReactiveChange[]) => {
		console.log("[Server Sync Plugin] Sending change sync request", changes);
		syncChangesToServer(changes).catch((error) => {
			console.error("[Server Sync Plugin] Sync operation failed:", error);
		});
	},
};

// --- Usage Example ---

// Define types for our reactive data
type UserPreferences = {
	theme: string;
	notificationsEnabled: boolean;
};

type UserProfile = {
	username: string;
	email: string;
	preferences: UserPreferences;
	tags: string[];
};

// Create a reactive user profile and attach the synchronization plugin.
// Note: by default, `createReactive` applies plugins deeply to nested objects
const userProfile = createReactive<UserProfile>(await getUserProfile(), {
	plugins: [ServerSyncPlugin],
});

runAction(() => {
	userProfile.username = "Jane Smith"; // Change on root object
	userProfile.preferences.theme = "light"; // Change on nested object
});
// Expected: Two calls to `syncChangesToServer`, one for 'userProfile' and one for 'userProfile.preferences':
// - LOG 1:
//   [Server Sync Plugin] Sending change sync request, [{
//     type: ReactiveChangeType.Object,
//     object: userProfile,
//     objectChange: {
//       type: ObjectChangeType.SetProperty,
//       property: "username",
//       previousValue: "Jack Black", // <- assuming initial `username` was `Jack Black`
//       nextValue: "Jane Smith",
//     }
//   }]
// - LOG 2:
//   [Server Sync Plugin] Sending change sync request, [{
//     type: ReactiveChangeType.Object,
//     object: userProfile.preferences,
//     objectChange: {
//       type: ObjectChangeType.SetProperty,
//       property: "theme",
//       previousValue: "dark", // <- assuming initial `theme` was `dark`
//       nextValue: "light",
//     }
//   }]

runAction(() => {
	userProfile.tags.push("Reactivity");
});
// Expected: One call to `syncChangesToServer` for `userProfile.tags`:
// - LOG:
//   [Server Sync Plugin] Sending change sync request, [{
//     type: ReactiveChangeType.Array,
//     array: userProfile.tags,
//     arrayChange: {
//       type: ArrayChangeType.PushValue,
//       index: 2, // <- assuming initial `tags` had 2 elements
//       nextValue: "Reactivity",
//     }
//   }]

runAction(() => {
	userProfile.tags.splice(0, 1, "Senior Developer"); // Replaces 1 element
});
// Expected: One call to `syncChangesToServer` for `userProfile.tags`:
// - LOG:
//   [Server Sync Plugin] Sending change sync request, [{
//     type: ReactiveChangeType.Array,
//     array: userProfile.tags,
//     arrayChange: {
//       type: ArrayChangeType.SetValue,
//       index: 0,
//       previousValue: "Intermediate Developer", // <- assuming 0th element of `tags` was `Intermediate Developer`
//       nextValue: "Senior Developer",
//     }
//   }]

Further Reading

To deepen your understanding of reactivity and related concepts: