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

@effex/core

v1.1.0

Published

Platform-agnostic reactivity primitives for Effex

Readme

@effex/core

Reactive primitives for Effex applications. This package provides the foundational reactivity system: Signals, Readables, async state management, reactive collections, state machines, and control flow primitives.

Note: @effex/dom re-exports everything from this package. If you're using @effex/dom, you don't need to install this separately.

Installation

pnpm add @effex/core effect

Signals

Signals are reactive values that can be read and updated:

import { Effect } from "effect";
import { Signal } from "@effex/core";

const count = yield* Signal.make(0);

// Read the current value
const current = yield* count.get;

// Update the value
yield* count.set(5);
yield* count.update((n) => n + 1);

Custom Equality

By default, Signal uses strict equality (===) to determine if a value has changed. Use Signal.equals for custom equality:

interface User {
  id: number;
  name: string;
  lastSeen: Date;
}

// Only trigger updates when the user ID changes
const currentUser = yield* Signal.make<User>(
  { id: 1, name: "Alice", lastSeen: new Date() }
).pipe(Signal.equals((a, b) => a.id === b.id));

This is useful for:

  • Objects with irrelevant fields (timestamps, metadata)
  • Expensive computations that shouldn't re-run on semantically equal inputs
  • Normalized data where you want to compare by ID rather than reference

Readable

Readables are the foundation of Effex's reactivity. They represent values that can be observed for changes. Signals are Readables, but you can also create derived Readables using combinators.

Derived Values

Use Readable.map to create derived values that automatically update:

import { Readable, Signal } from "@effex/core";

const firstName = yield* Signal.make("John");
const lastName = yield* Signal.make("Doe");

// Simple derivation with map
const upperFirst = Readable.map(firstName, (s) => s.toUpperCase());

// Combine multiple readables
const fullName = Readable.zipWith(
  firstName,
  lastName,
  (first, last) => `${first} ${last}`
);

// Or use zipAll for tuples
const both = Readable.zipAll([firstName, lastName]);
// both: Readable<[string, string]>

Constants and Streams

// Constant (never changes)
const label = Readable.of("Hello");

// From an initial value + stream of updates
const time = Readable.fromStream(Date.now(), clockStream);

Normalizing Props

Many functions accept props that can be either static values or reactive Readable values. Use Readable.normalize() to handle both cases:

// Reactive<T> means: T | Readable<T>
interface ButtonProps {
  disabled?: Readable.Reactive<boolean>;
  class?: Readable.Reactive<string>;
}

const Button = <E, R>(props: ButtonProps, child: Child<E, R>) => {
  // Normalize props - works whether they're static or reactive
  const disabled = Readable.normalize(props.disabled ?? false);
  const className = Readable.normalize(props.class ?? "");

  // Use .pipe(Readable.map()) for derived attributes
  const ariaDisabled = disabled.pipe(
    Readable.map((d) => (d ? "true" : undefined))
  );

  return $.button(
    { class: className, disabled, "aria-disabled": ariaDisabled },
    child
  );
};

Lifting Functions

When using utility libraries like class-variance-authority (CVA) or clsx, use Readable.lift to make them reactive-aware:

import { cva } from "class-variance-authority";
import { Signal, Readable } from "@effex/core";

const buttonStyles = cva("btn font-medium rounded", {
  variants: {
    variant: { primary: "bg-blue-500", secondary: "bg-gray-200" },
    size: { sm: "px-2 py-1", md: "px-4 py-2", lg: "px-6 py-3" },
  },
});

// Lift it to accept Readables
const reactiveButtonStyles = Readable.lift(buttonStyles);

const variant = yield* Signal.make<"primary" | "secondary">("primary");

// className is Readable<string> - updates when variant changes
const className = reactiveButtonStyles({ variant, size: "md" });

Ref

Mutable references with deferred resolution, similar to React's useRef but with Effect integration:

import { Ref } from "@effex/core";

// Create a ref for a DOM element
const inputRef = yield* Ref.make<HTMLInputElement>();

// inputRef.current is null until set
// inputRef.value is an Effect that resolves when the ref is populated

// Later, focus the input (waits until the ref is set)
yield* inputRef.value.pipe(
  Effect.tap((el) => Effect.sync(() => el.focus()))
);

AsyncReadable

For async operations like data fetching, use AsyncReadable. It provides reactive state for loading, value, and error:

import { AsyncReadable } from "@effex/core";

const userData = yield* AsyncReadable.make(() =>
  Effect.gen(function* () {
    const response = yield* fetchUser(userId);
    return response.data;
  })
);

// Reactive state properties
userData.isLoading;  // Readable<boolean>
userData.value;      // Readable<Option<User>>
userData.error;      // Readable<Option<Error>>

// Manually trigger refetch
yield* userData.refetch();

// Reset to initial state
yield* userData.reset();

From Promises

// Simple promise
const data = yield* AsyncReadable.promise(() => fetch("/api/data").then(r => r.json()));

// With error handling
const data = yield* AsyncReadable.tryPromise(
  () => fetch("/api/data").then(r => r.json()),
  (error) => new ApiError({ cause: error })
);

Reactive Dependencies

Recompute when a source Readable changes:

const userId = yield* Signal.make("alice");

const profile = yield* AsyncReadable.fromReadable(
  (id) => Effect.tryPromise(() => fetchProfile(id))
)(userId);
// Refetches automatically when userId changes

Use with control flow primitives:

import { when, matchOption } from "@effex/core";

// Show loading spinner
when(userData.isLoading, {
  onTrue: () => Spinner(),
  onFalse: () => $.span(),
});

// Handle the value (matchOption unwraps Option for you)
matchOption(userData.value, {
  onSome: (user) => UserCard({ user }),  // user is Readable<User>
  onNone: () => $.span({}, $.of("No data")),
});

AsyncCache

A caching layer for async operations. Entries are keyed by hierarchical paths and can be invalidated by prefix. Ideal for data fetching in apps with loaders and mutations.

import { AsyncCache } from "@effex/core";

const cache = yield* AsyncCache;

// Get or create a cached async readable
const posts = yield* cache.get(
  ["posts"],
  () => Effect.tryPromise(() => fetch("/api/posts").then(r => r.json())),
);

// Seed with initial data (e.g., from a server-side loader)
const posts = yield* cache.get(
  ["posts"],
  () => Effect.tryPromise(() => fetch("/api/posts").then(r => r.json())),
  { initialData: loaderData.posts },
);

Cache Keys

Keys are arrays of primitives, enabling hierarchical invalidation:

// Key examples
["posts"]                    // all posts
["posts", "feed"]            // feed posts specifically
["posts", userId]            // posts by user
["users", 42, true]          // compound keys

Invalidation

// Invalidate all entries starting with ["posts"] — triggers refetch
yield* cache.invalidate(["posts"]);

// Invalidate a specific user's data
yield* cache.invalidate(["users", userId]);

// Remove entries entirely (no refetch)
yield* cache.remove(["posts"]);

// Clear the whole cache
yield* cache.clear();

Typical Usage with Loaders

const FeedPage = (data: { posts: Post[] }) =>
  Effect.gen(function* () {
    const cache = yield* AsyncCache;

    // Seed cache with server-loaded data, fetch from client on invalidation
    const feedQuery = yield* cache.get(
      ["feed"],
      () => Effect.tryPromise(() =>
        fetch("/?_data=1").then(r => r.json()).then(r => r.data)
      ),
      { initialData: data },
    );

    const posts = Readable.map(feedQuery.value, (fetched) =>
      Option.match(fetched, {
        onSome: (f) => f.posts,
        onNone: () => data.posts,
      }),
    );

    // After a mutation, invalidate to refetch:
    yield* cache.invalidate(["feed"]);
  });

Mutation

For explicit async operations (like form submissions, API calls), use Mutation. Unlike AsyncReadable, mutations are triggered manually:

import { Mutation } from "@effex/core";

const createUser = yield* Mutation.make((input: CreateUserInput) =>
  Effect.gen(function* () {
    const response = yield* api.createUser(input);
    return response.user;
  })
);

// Reactive state
createUser.isLoading;  // Readable<boolean>
createUser.data;       // Readable<Option<User>>
createUser.error;      // Readable<Option<Error>>

// Execute the mutation
const user = yield* createUser.run({ name: "Alice", email: "[email protected]" });

// Reset state
yield* createUser.reset();

From Promises

const createUser = yield* Mutation.promise((input: CreateUserInput) =>
  fetch("/api/users", { method: "POST", body: JSON.stringify(input) }).then(r => r.json())
);

// With error handling
const createUser = yield* Mutation.tryPromise(
  (input: CreateUserInput) =>
    fetch("/api/users", { method: "POST", body: JSON.stringify(input) }).then(r => r.json()),
  (error) => new ApiError({ cause: error })
);

Reactive Collections

Effex provides reactive versions of Array, Map, Set, and Struct. Unlike in React where you must clone collections on every mutation, these allow in-place mutations that automatically trigger reactive updates.

Signal.Array

A reactive array with in-place mutation methods:

const todos = yield* Signal.Array.make<Todo>([]);

// In-place mutations (no cloning needed!)
yield* todos.push({ id: 1, text: "Learn Effex", done: false });
yield* todos.unshift(firstItem);
yield* todos.pop();
yield* todos.shift();
yield* todos.splice(1, 2, replacement);
yield* todos.insertAt(0, item);
yield* todos.removeAt(index);
yield* todos.remove(specificItem);  // By reference
yield* todos.move(fromIndex, toIndex);  // Great for drag-and-drop
yield* todos.swap(indexA, indexB);
yield* todos.sort((a, b) => a.id - b.id);
yield* todos.reverse();
yield* todos.clear();

// Transform entire array
yield* todos.update(arr => arr.filter(t => !t.done));
yield* todos.set(newTodos);

// Reactive length
todos.length;  // Readable<number>

// Use with each
each(todos, {
  key: todo => todo.id,
  render: todo => TodoItem(todo),
});

Signal.Map

A reactive Map for key-value stores:

const users = yield* Signal.Map.make<string, User>();

// Mutations
yield* users.set("u1", { name: "Alice", role: "admin" });
yield* users.delete("u1");
yield* users.clear();
yield* users.replace(newMap);
yield* users.update(m => new Map([...m, ["u2", bob]]));

// Reactive reads (for UI binding)
users.at("u1");              // Readable<Option<User>>
users.atOrElse("u1", guest); // Readable<User>
users.has("u1");              // Readable<boolean>

// One-time reads (for imperative code)
const user = yield* users.atEffect("u1");   // Effect<Option<User>>
const exists = yield* users.hasEffect("u1"); // Effect<boolean>

// Reactive derived values
users.size;         // Readable<number>
users.entries;      // Readable<readonly [string, User][]>
users.keys;         // Readable<readonly string[]>
users.valuesArray;  // Readable<readonly User[]>

Signal.Set

A reactive Set for unique collections:

const tags = yield* Signal.Set.make<string>(["draft"]);

// Mutations
yield* tags.add("important");
yield* tags.delete("draft");
yield* tags.toggle("featured");  // Add if missing, remove if present
yield* tags.clear();
yield* tags.replace(newSet);
yield* tags.update(s => new Set([...s, "extra"]));

// Reactive reads
tags.has("important");  // Readable<boolean>

// One-time read
const exists = yield* tags.hasEffect("important");  // Effect<boolean>

// Reactive derived values
tags.size;         // Readable<number>
tags.valuesArray;  // Readable<readonly string[]>

Why This Matters: In React, mutating a Map/Set doesn't trigger re-renders because the reference is unchanged. You're forced to clone on every mutation:

// React (painful)
setMap(new Map(map).set(key, value));
setSet(new Set(set).add(item));

With Effex's reactive collections, mutations are O(1) and automatically trigger updates.

Signal.Struct

A reactive object where each field is an individual Signal. This allows granular updates to specific fields without affecting the rest:

const address = yield* Signal.Struct.make({
  street: "123 Main St",
  city: "Austin",
  zip: "78701",
});

// Each field is a Signal — granular reads and writes
yield* address.street.set("456 Oak Ave");
yield* address.city.update((c) => c.toUpperCase());

// Read the whole struct as a single Readable
const value = yield* address.get;
// { street: "456 Oak Ave", city: "AUSTIN", zip: "78701" }

// Batch update multiple fields
yield* address.update({ street: "789 Pine Rd", city: "Houston" });

// Replace the entire struct
yield* address.replace({ street: "100 New St", city: "San Antonio", zip: "78201" });

// List of field keys
address.keys;  // readonly ["street", "city", "zip"]

Transition (State Machines)

Transition provides declarative state machines with type-safe transitions and reactive guards:

import { Transition } from "@effex/core";

const status = yield* Transition.make(
  {
    idle: ["loading"],
    loading: ["success", "error"],
    success: ["idle"],
    error: ["idle", "loading"],
  },
  "idle"
);

// Current state (read-only Readable)
status.current;  // Readable<"idle" | "loading" | "success" | "error">

// Transition to a new state (fails if not allowed)
yield* status.to("loading");  // Effect<void, InvalidTransition>

// Check current state (reactive)
status.is("idle");  // Readable<boolean>

// Check if transition is allowed (reactive)
status.canTransitionTo("success");  // Readable<boolean>

Guarded Transitions

Add reactive guards to transitions that must be satisfied:

const isOnline = yield* Signal.make(true);

const status = yield* Transition.make(
  {
    idle: [{ to: "loading", when: isOnline }, "error"],  // guarded + unguarded
    loading: ["success", "error"],
    success: ["idle"],
    error: ["idle"],
  },
  "idle"
);

// canTransitionTo respects guards - updates reactively
status.canTransitionTo("loading");  // true only when isOnline is true

// Transition fails if guard is false
yield* status.to("loading");  // InvalidTransition if offline

Guarded Callbacks

Create callbacks that only run when in specific states:

const submit = status.guard(
  ["idle"],  // only enabled in these states
  (data: FormData) => Effect.gen(function* () {
    yield* status.to("loading");
    return yield* api.submit(data);
  }),
  { onBlocked: "ignore" }  // or "fail" (default)
);

yield* submit(formData);

Control Flow

Reactive control flow primitives for conditional and list rendering. These work with any Readable value and automatically update the UI when the source changes.

when

Conditional rendering based on a boolean Readable:

import { when } from "@effex/core";

when(isLoggedIn, {
  onTrue: () => Dashboard(),
  onFalse: () => LoginForm(),
});

match

Pattern matching on a Readable value:

import { match } from "@effex/core";

match(status.current, {
  cases: [
    { pattern: "idle", render: () => IdleView() },
    { pattern: "loading", render: () => LoadingSpinner() },
    { pattern: "error", render: () => ErrorView() },
  ],
  fallback: () => $.div({}, $.of("Unknown state")),
});

matchOption / matchEither

Handle Option and Either values reactively:

import { matchOption, matchEither } from "@effex/core";

matchOption(userData.value, {
  onSome: (user) => UserCard({ user }),  // user is Readable<User>
  onNone: () => $.span({}, $.of("No data")),
});

matchEither(result, {
  onRight: (value) => SuccessView({ value }),
  onLeft: (error) => ErrorView({ error }),
});

each

Keyed list rendering with efficient reconciliation:

import { each } from "@effex/core";

each(todos, {
  key: (todo) => todo.id,
  render: (todo, index) => TodoItem({ todo, index }),
  container: () => $.ul({ class: "todo-list" }),
});

redraw

Complete redraw on every change (useful when the whole subtree depends on the value):

import { redraw } from "@effex/core";

redraw(theme, {
  render: (currentTheme) => ThemedApp({ theme: currentTheme }),
});

API Reference

Signal

| Method | Signature | Description | |--------|-----------|-------------| | Signal.make | (initial: T) => Effect<Signal<T>, never, Scope> | Create a signal | | Signal.equals | (fn: (a, b) => boolean) => (signal) => signal | Custom equality (pipeable) | | Signal.fromNullable | (existing?, default) => Effect<Signal<T>> | Use existing or create new | | Signal.fromReactive | (value, default) => Effect<Signal<T>> | Convert any reactive value to Signal | | signal.get | Effect<T> | Read current value | | signal.set | (value: T) => Effect<void> | Set value | | signal.update | (fn: (T) => T) => Effect<void> | Update with function |

Readable

| Method | Signature | Description | |--------|-----------|-------------| | Readable.of | (value: T) => Readable<T> | Constant readable | | Readable.normalize | (value: T \| Readable<T>) => Readable<T> | Normalize static or reactive | | Readable.fromStream | (initial, stream) => Readable<T> | From initial + stream | | Readable.map | (readable, fn) => Readable<B> | Transform value | | Readable.flatMap | (readable, fn) => Readable<B> | Chain readables | | Readable.zip | (a, b) => Readable<[A, B]> | Combine into tuple | | Readable.zipWith | (a, b, fn) => Readable<C> | Combine with function | | Readable.zipAll | (readables) => Readable<[...]> | Combine multiple into tuple | | Readable.filter | (readable, predicate) => Readable<T> | Filter values | | Readable.dedupe | (readable) => Readable<T> | Remove consecutive duplicates | | Readable.dedupeWith | (readable, equals) => Readable<T> | Dedupe with custom equality | | Readable.lift | (fn) => reactiveFn | Lift function to accept Readables | | Readable.tap | (readable, fn) => Readable<T> | Side effect on each value | | readable.get | Effect<T> | Read current value | | readable.changes | Stream<T> | Stream of changes | | readable.values | Stream<T> | Stream of all values (current + changes) |

Ref

| Method | Signature | Description | |--------|-----------|-------------| | Ref.make | () => Effect<Ref<T>, never, Scope> | Create empty ref | | ref.current | T \| null | Mutable current value | | ref.value | Effect<T> | Resolves when ref is set | | ref.set | (value: T) => void | Set value and complete deferred |

AsyncReadable

| Method | Signature | Description | |--------|-----------|-------------| | AsyncReadable.make | (fn) => Effect<AsyncReadable<A, E>> | From effect (fetches immediately) | | AsyncReadable.promise | (fn) => Effect<AsyncReadable<A, unknown>> | From promise | | AsyncReadable.tryPromise | (fn, onError) => Effect<AsyncReadable<A, E>> | From promise with error mapping | | AsyncReadable.fromReadable | (fn) => (source) => Effect<AsyncReadable> | Recompute on source change | | AsyncReadable.map | (ar, fn) => AsyncReadable<B, E> | Transform successful value | | ar.isLoading | Readable<boolean> | Loading state | | ar.value | Readable<Option<A>> | Successful value | | ar.error | Readable<Option<E>> | Error state | | ar.refetch | () => Effect<void> | Manually refetch | | ar.reset | () => Effect<void> | Reset to initial state |

AsyncCache

| Method | Signature | Description | |--------|-----------|-------------| | AsyncCache | Context.Tag | Service tag (use yield* AsyncCache) | | makeAsyncCache | () => IAsyncCache | Create instance | | cache.get | (key, fetcher, options?) => Effect<AsyncReadable> | Get or create cached entry | | cache.invalidate | (keyPrefix) => Effect<void> | Invalidate + refetch matching entries | | cache.remove | (keyPrefix) => Effect<void> | Remove matching entries | | cache.clear | () => Effect<void> | Clear entire cache |

Mutation

| Method | Signature | Description | |--------|-----------|-------------| | Mutation.make | (fn) => Effect<Mutation<I, O, E>> | From effect | | Mutation.promise | (fn) => Effect<Mutation<I, O, unknown>> | From promise | | Mutation.tryPromise | (fn, onError) => Effect<Mutation<I, O, E>> | From promise with error mapping | | Mutation.map | (mutation, fn) => Mutation<I, O2, E> | Transform result | | Mutation.flatMap | (mutation, fn) => Mutation<I, O2, E\|E2> | Chain mutations | | mutation.isLoading | Readable<boolean> | Loading state | | mutation.data | Readable<Option<O>> | Successful result | | mutation.error | Readable<Option<E>> | Error state | | mutation.run | (input: I) => Effect<O, E> | Execute mutation | | mutation.reset | () => Effect<void> | Reset to initial state |

Reactive Collections

| Constructor | Description | |-------------|-------------| | Signal.Array.make<T>(initial?) | Reactive array | | Signal.Map.make<K, V>(initial?) | Reactive map | | Signal.Set.make<T>(initial?) | Reactive set | | Signal.Struct.make<T>(initial) | Reactive struct with per-field Signals |

Transition

| Method | Signature | Description | |--------|-----------|-------------| | Transition.make | (config, initial) => Effect<Transition<S>> | Create state machine | | transition.current | Readable<S> | Current state | | transition.to | (state) => Effect<void, InvalidTransition> | Transition | | transition.is | (state) => Readable<boolean> | Check state | | transition.canTransitionTo | (state) => Readable<boolean> | Check if allowed | | transition.guard | (states, callback, options?) => guardedFn | Create guarded callback |

Control Flow

| Function | Description | |----------|-------------| | when(condition, { onTrue, onFalse }) | Conditional rendering | | match(value, { cases, fallback }) | Pattern matching | | matchOption(option, { onSome, onNone }) | Option matching | | matchEither(either, { onRight, onLeft }) | Either matching | | each(items, { key, render, container }) | Keyed list rendering | | redraw(readable, { render, container }) | Full redraw on change |