Skip to content

efficimo/observable

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@efficimo/observable

npm version npm version license types

Lightweight observable state primitives for TypeScript: stateful values, bidirectional derived values, object path subscriptions, JSON serialization with Zod, and a React hook.

Packages

Package Description
@efficimo/observable Core primitives — framework-agnostic
@efficimo/observable-react React bindings — useObservableValueState hook

Why

The observer pattern is everywhere: event emitters, stores, reactive forms, URL state sync. But most implementations are either too heavy (RxJS), too tied to a specific framework, or offer no stateful "current value" semantics.

@efficimo/observable provides a minimal, composable set of primitives:

  • Observable — push notifications, no state
  • ObservableValue — holds a current value, emits immediately on subscribe, supports async setter functions, skips duplicate values via deep equality
  • DerivedObservableValue — bidirectional derived value: changes in the source propagate forward, changes in the derived propagate back
  • ObjectObservableValue — observe an object as a whole or any nested path independently, fully bidirectional
  • JsonSerializeObservableValue — bridge between a string | null observable (e.g. localStorage, URL params) and a typed value via Zod validation

Installation

# core only
npm install @efficimo/observable

# with React hook
npm install @efficimo/observable @efficimo/observable-react

Quick start

import { ObservableValue } from '@efficimo/observable';

const count = new ObservableValue(0);

// subscribes and receives current value immediately
count.subscribe(v => console.log('count:', v)); // count: 0

await count.next(1);  // count: 1

// functional setter (like React's setState)
await count.next(prev => prev + 1);  // count: 2

// deep-equal check — no notification emitted
await count.next(2);  // (silence)

API

Observable<Value>

Base class. Stateless push channel.

new Observable<Value>()
Member Description
subscribe(fn) Register a subscriber. Returns a Subscription with unsubscribe().
next(value) Notify all current subscribers.

ObservableValue<Value>

Extends Observable<Value>. Holds a current value.

new ObservableValue<Value>(initialValue)
Member Description
getValue() Returns the current value.
subscribe(fn) Registers subscriber and immediately calls it with the current value.
next(value | setter) Updates the value. Supports async setter (prev) => newValue. No-op if deep-equal to current.
const obs = new ObservableValue({ count: 0 });

obs.subscribe(v => console.log(v)); // { count: 0 }

await obs.next({ count: 1 });       // { count: 1 }
await obs.next(prev => ({ ...prev, count: prev.count + 1 })); // { count: 2 }

DerivedObservableValue<Value, DerivedValue>

Extends ObservableValue<Value>. Bidirectional link between two observables via from/to converters.

new DerivedObservableValue(source, from, to, defaultValue?)
Parameter Description
source The upstream ObservableValueInterface<DerivedValue>
from (derivedValue: DerivedValue) => Value — convert source → derived
to (value: Value) => DerivedValue — convert derived → source
const source = new ObservableValue(42);
const asString = new DerivedObservableValue(
  source,
  n => String(n),
  s => Number(s),
);

await source.next(100);
console.log(asString.getValue()); // "100"

await asString.next("200");
console.log(source.getValue());   // 200

ObjectObservableValue<Value>

Wraps an object observable with typed path-based subscriptions.

new ObjectObservableValue<Value>(valueOrObservable)
Member Description
getValue() Current object value.
subscribe(fn) Subscribe to the full object.
next(value | setter) Update the full object.
getPartObservable(path) Returns an ObservableValue for a nested path. Bidirectional.
type State = { user: { name: string; age: number } };

const state = new ObjectObservableValue<State>({ user: { name: 'Alice', age: 30 } });

const nameObs = state.getPartObservable('user.name');
nameObs.subscribe(name => console.log('name:', name)); // name: Alice

await nameObs.next('Bob');
console.log(state.getValue().user.name); // Bob

JsonSerializeObservableValue<Value>

Extends DerivedObservableValue. Serializes/deserializes JSON with schema validation. Useful for URL params, localStorage, or any string-based store.

Works with any validation library exposing a safeParse method (Zod, Valibot, etc.) via the structural SafeParseSchema<Value> interface — no direct dependency.

new JsonSerializeObservableValue(source, schema, defaultValue?)
import { z } from 'zod';
import { ObservableValue, JsonSerializeObservableValue } from '@efficimo/observable';

const raw = new ObservableValue<string | null>(null);
const filters = new JsonSerializeObservableValue(raw, z.object({ page: z.number() }));

await raw.next('{"page":2}');
console.log(filters.getValue()); // { page: 2 }

await filters.next({ page: 3 });
console.log(raw.getValue()); // '{"page":3}'

Subscription

Returned by subscribe().

Member Description
unsubscribe() Removes the subscriber.

React bindings — @efficimo/observable-react

npm install @efficimo/observable @efficimo/observable-react

useObservableValueState

Drop-in replacement for useState, backed by an ObservableValue.

import { ObservableValue } from '@efficimo/observable';
import { useObservableValueState } from '@efficimo/observable-react';

const count = new ObservableValue(0);

function Counter() {
  const [value, setValue] = useObservableValueState(count);

  return (
    <button onClick={() => setValue(prev => prev + 1)}>
      Count: {value}
    </button>
  );
}

Multiple components subscribing to the same ObservableValue stay in sync automatically. The observable lives outside React — no context, no provider, no boilerplate.


Local development

# build core first
cd core && npm ci && npm run build

# then work on react bindings
cd ../react && npm ci && npm run typecheck

Publishing

Tag Publishes
v1.2.3 @efficimo/observable@1.2.3
v1.2.3 @efficimo/observable-react@1.2.3

License

MIT

About

Lightweight TypeScript observable state primitives with deep-equality diffing, bidirectional derived values, and React bindings.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors