observite-js
v1.0.0
Published
A lightweight global state management library with automatic memory cleanup and dynamic state containers.
Maintainers
Readme
Overview
Observite is a lightweight state management library built around three core primitives:
- Observables — Units of shared state (sync or async) that notify subscribers when they change
- Selectors — Pure functions that derive new state from Observables or other Selectors
- Observers — Subscribe to Observables and Selectors, reacting when values update
These primitives form a data-flow graph: state flows from Observables through Selectors and into your application via Observers.
Observite offers:
- Flexible subscriptions — Subscribe to state changes conditionally, in any order, and even within loops or dynamic contexts
- Controlled memory management — Create state dynamically and configure exactly when it gets released
- Framework-agnostic design — Works in React, vanilla JS, or any JavaScript environment
- First-class async support — Built-in Promise handling with pending/resolved/rejected states
Installation
npm install observite-jsNote: This package was previously published as
observite, but that name was taken. The official package is nowobservite-js.
Core Concepts
Observables
Observables are units of state. They're updatable and subscribable: when an observable is updated, each subscriber is notified with the new value.
Create your observables in a freely accessible place, like a global context or a module:
const fontSize = new Observable<number>({
default: 14,
});Subscribe to them in your application code. For example, in React it might look something like this:
const {observe} = useObserver();
return <FontSlider
value={observe(fontSize)} // Subscribe to fontSize
onChange={fontSize.set} // Update fontSize
/>;Selectors
Selectors are used to calculate derived data that is based on other state.
const fontSizeLabel = new Selector<string>(({ observe }) => {
// Every time fontSize changes, fontSizeLabel updates
const size = observe(fontSize);
return `${size}px`;
});They are subscribable just like observables. You can use them anywhere you'd use an observable:
const {observe} = useObserver();
return <FontSlider
label={observe(fontSizeLabel)}
value={observe(fontSize)}
onChange={fontSize.set}
/>;Observers
Observers are the bridge between your state and your application. They subscribe to state changes and notify your code when updates occur.
Usually you will wrap observer creation based on your framework or environment. For example, in React you might use useObserver().
Example without a framework:
const {observe} = new SyncObserver();
// On changes, subscriptions are reset so that you can subscribe to different state.
// Each time, you should observe the state you want to use until the next change.
const render = () => {
const label = observe(fontSizeLabel);
document.querySelector('#font-size-label').textContent = label;
};
// Set up onChange and subscribe for the first time
observer.setOnChange(render);
render(); // label: "14px"
fontSize.set(16); // label: "16px"
fontSize.set(18); // label: "18px"Dynamic Subscriptions
Observite allows fully dynamic subscriptions. On each update, you can change what you're subscribed to freely—conditionally, in loops, or in any order.
Conditional subscriptions:
const detailsSelector = new Selector<Details | null>(({ observe }) => {
const showDetails = observe(showDetailsState);
// Only subscribe to details when needed
if (showDetails) {
return observe(expensiveDetailsState);
}
return null;
});Subscriptions in loops:
const totalSelector = new Selector<number>(({ observeKey }) => {
const itemIds = ['item-1', 'item-2', 'item-3'];
// Subscribe to a dynamic list of items
let total = 0;
for (const id of itemIds) {
const item = observeKey(itemPriceMap, id);
total += item?.price ?? 0;
}
return total;
});Changing subscription sets:
const dataSelector = new Selector<Data>(({ observe }) => {
const mode = observe(modeState);
// Different subscriptions based on current mode
switch (mode) {
case 'simple':
return observe(simpleDataState);
case 'detailed':
return observe(simpleDataState) + observe(extraDataState);
case 'full':
return observe(simpleDataState) + observe(extraDataState) + observe(metaDataState);
}
});This flexibility means your selectors can efficiently subscribe to exactly what they need based on the current state, automatically unsubscribing from unused dependencies when conditions change.
Async Observables
Observite has first-class support for asynchronous state. Use AsyncObservable and AsyncSelector for data that comes from promises:
const userDataState = new AsyncSelector<UserData>(async ({ observe }) => {
const userId = observe(currentUserIdState);
const response = await fetch(`/api/users/${userId}`);
return response.json();
});Async selectors automatically track their loading state. When the promise is pending, observers can choose how to handle it:
- AsyncObserver — Returns the promise for async/await handling
// Waits until userData is loaded before logging
const data = await observe(userData);
console.log(`Hello, ${data.name}!`);- ComponentObserver — Throws the promise for React Suspense integration
function UserProfile() {
const { observe } = useObserver(); // wraps ComponentObserver
// Throws promise to Suspense if still loading
const userData = observe(userDataState);
return <div>Hello, {userData.name}!</div>;
}- observeState — Returns the raw state object with status, result, and error
function UserProfile() {
const { observeState } = useObserver();
const result = observeState(userDataState);
if (result.status === Status.Pending) {
return <Loading />;
}
if (result.status === Status.Rejected) {
return <Error error={result.error} />;
}
return <div>Hello, {result.result.name}!</div>;
}Keyed Collections
For dynamic data where you need multiple instances of state keyed by an identifier, use ObservableMap and SelectorMap:
const fieldValidation = new SelectorMap<string, string | null>(
({ observe }, fieldName) => {
const formData = observe(formState);
const value = formData[fieldName];
return validateField(fieldName, value);
}
);
// Access data by key
const emailValidation = observeKey(fieldValidation, 'email');Memory Management
When no observers are watching an observable or selector, Observite can automatically clean up that entry instantly, never, or after a configurable delay.
const cachedData = new Selector<ExpensiveData>(
({ observe }) => fetchData(observe(inputState)),
{
releaseDelay: 30000, // Keep cached for 30 seconds after last observer leaves
onRelease: (data) => {
console.log('Cleaning up cached data');
},
}
);API Reference
Observables
| Class | Description |
|-------|-------------|
| Observable<T> | Basic synchronous state container |
| AsyncObservable<T> | State container for Promises |
| ObservableMap<K, V> | Keyed collection of sync observables |
| AsyncObservableMap<K, V> | Keyed collection of async observables |
| ObservableAnimation<T> | Animated transitions between values |
Observable Options
new Observable<T>({
default?: T, // Initial value
debugID?: string, // Unique identifier for debugging
onChanged?: (value: T) => void, // Callback on value change
releaseDelay?: number | ReleaseDelay, // Cleanup delay in ms
onRelease?: (value: T) => void, // Cleanup callback
comparisonMethod?: ComparisonMethod, // Equality check method
});Observable Methods
observable.set(value) // Update the value
observable.peek() // Get current value without subscribing
observable.isInitialized() // Check if value has been set
observable.destroy() // Clean up the observableSelectors
| Class | Description |
|-------|-------------|
| Selector<T> | Derived synchronous state |
| AsyncSelector<T> | Derived async state |
| SelectorMap<K, V> | Keyed collection of sync selectors |
| AsyncSelectorMap<K, V> | Keyed collection of async selectors |
Selector Methods
selector.peek() // Get current value without subscribing
selector.peekSafe() // Get current value if initialized, otherwise null
selector.destroy() // Clean up the selectorObservers
| Class | Description |
|-------|-------------|
| SyncObserver | For synchronous-only state |
| AsyncObserver | Returns promises for pending async state |
| ComponentObserver | Throws promises for React Suspense |
Observer Methods
observer.setOnChange(callback) // Set change notification callback
observer.observe(observable) // Subscribe and get current value
observer.observeKey(map, key) // Subscribe to a keyed entry
observer.observeState(observable) // Subscribe and get raw state with status
observer.observeKeyState(map, key) // Subscribe to keyed entry raw state
observer.destroy() // Unsubscribe from all observables
observer.reset() // Reset without destroyingContributing
Development of Observite happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Observite.
License
Observite is MIT licensed.
