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

@assistant-ui/tap

v0.5.2

Published

Zero-dependency reactive state management inspired by React hooks

Readme

@assistant-ui/tap

tap (Reactive Resources) is a zero-dependency reactive state management library that brings React's hooks mental model to state management outside of React components.

Installation

npm install @assistant-ui/tap

What is tap?

Instead of limiting hooks to React components, tap lets you use the same familiar hooks pattern (useState, useEffect, useMemo, etc.) to create self-contained, reusable units of reactive state and logic called Resources that can be used anywhere - in vanilla JavaScript, servers, or outside of React.

Philosophy

  • Unified mental model: Use the same hooks pattern everywhere
  • Framework agnostic: Zero dependencies, works with or without React
  • Lifecycle management: Resources handle their own cleanup automatically
  • Type-safe: Full TypeScript support with proper type inference

How It Works

tap implements a render-commit pattern similar to React:

Render Phase

  1. Each resource instance has a "fiber" that tracks state and effects
  2. When a resource function runs, hooks record their data in the fiber
  3. The library maintains an execution context to track which fiber's hooks are being called
  4. Each hook stores its data in cells indexed by call order (enforcing React's rules)

Commit Phase

  1. After render, collected effect tasks are processed
  2. Effects check if dependencies changed using shallow equality
  3. Old effects are cleaned up before new ones run
  4. Updates are batched using microtasks to prevent excessive re-renders

Core Concepts

Resources

Resources are self-contained units of reactive state and logic. They follow the same rules as React hooks:

  • Hook Order: Hooks must be called in the same order in every render
  • No Conditional Hooks: Can't call hooks inside conditionals or loops
  • No Async Hooks: Hooks must be called synchronously during render
  • Resources automatically handle cleanup and lifecycle

Creating Resources

import { createResourceRoot, tapState, tapEffect } from "@assistant-ui/tap";

// Define a resource using familiar hook patterns
const Counter = resource(({ incrementBy = 1 }: { incrementBy?: number }) => {
  const [count, setCount] = tapState(0);

  tapEffect(() => {
    console.log(`Count is now: ${count}`);
  }, [count]);

  return {
    count,
    increment: () => setCount((c) => c + incrementBy),
    decrement: () => setCount((c) => c - incrementBy),
  };
});

// Create an instance
const root = createResourceRoot();
const counter = root.render(Counter({ incrementBy: 2 }));

// Subscribe to changes
const unsubscribe = counter.subscribe(() => {
  console.log("Counter value:", counter.getValue().count);
});

// Use the resource
counter.getValue().increment();

resource

Creates a resource element factory. Resource elements are plain objects of the type { type: ResourceFn<R, P>, props: P, key?: string | number }.

const Counter = resource(({ incrementBy = 1 }: { incrementBy?: number }) => {
  const [count, setCount] = tapState(0);
});

// create a Counter element
const counterEl = Counter({ incrementBy: 2 });

// create a Counter instance
const root = createResourceRoot();
root.render(counterEl);
root.unmount();

Hook APIs

tapState

Manages local state within a resource, exactly like React's useState.

const [value, setValue] = tapState(initialValue);
const [value, setValue] = tapState(() => computeInitialValue());

tapEffect

Runs side effects with automatic cleanup, exactly like React's useEffect.

tapEffect(() => {
  // Effect logic
  return () => {
    // Cleanup logic
  };
}, [dependencies]);

tapMemo

Memoizes expensive computations, exactly like React's useMemo.

const expensiveValue = tapMemo(() => {
  return computeExpensiveValue(dep1, dep2);
}, [dep1, dep2]);

tapCallback

Memoizes callbacks to prevent unnecessary re-renders, exactly like React's useCallback.

const stableCallback = tapCallback(() => {
  doSomething(value);
}, [value]);

tapRef

Creates a mutable reference that persists across renders, exactly like React's useRef.

// With initial value
const ref = tapRef(initialValue);
ref.current = newValue;

// Without initial value
const ref = tapRef<string>(); // ref.current is undefined
ref.current = "hello";

tapResource

Composes resources together - resources can render other resources.

const Timer = resource(() => {
  const counter = tapResource({ type: Counter, props: { incrementBy: 1 } });

  tapEffect(() => {
    const interval = setInterval(() => {
      counter.increment();
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return counter.count;
});

tapResources

Renders multiple resources from an array, similar to React's list rendering. Returns an array with each resource's result.

tapResources<E extends ResourceElement<any, any>>(
  getElements: () => readonly E[],
  getElementsDeps: readonly any[]
): ExtractResourceReturnType<E>[]

Parameters:

  • getElements: A function that returns an array of ResourceElements
  • getElementsDeps: Dependency array for memoizing the getElements function

Example:

const TodoItem = resource((props: { text: string }) => {
  const [completed, setCompleted] = tapState(false);
  return { text: props.text, completed, setCompleted };
});

const TodoList = resource(() => {
  const todos = tapMemo(
    () => [
      { id: "1", text: "Learn tap" },
      { id: "2", text: "Build something awesome" },
    ],
    [],
  );

  // Returns Array<{ text, completed, setCompleted }>
  const todoItems = tapResources(
    () => todos.map((todo) => TodoItem({ text: todo.text })),
    [todos]
  );

  return todoItems;
});

Key features:

  • Resource instances are preserved when keys remain the same (use withKey() to provide stable keys)
  • Automatically cleans up resources when removed from the array
  • Handles resource type changes (recreates fiber if type changes)

tap and Context Support

Create and use context to pass values through resource boundaries without prop drilling.

import {
  createResourceContext,
  tap,
  withContextProvider,
} from "@assistant-ui/tap";

const MyContext = createResourceContext(defaultValue);

// Provide context
withContextProvider(MyContext, value, () => {
  // Inside this function, tap can access the value
});

// Access context in a resource
const value = tap(MyContext);

Resource Management

createResourceRoot

Create an instance of a resource. Call render() with a resource element to render it and mount effects. Returns a SubscribableResource with getValue() and subscribe().

import { createResourceRoot } from "@assistant-ui/tap";

const root = createResourceRoot();
const counter = root.render(Counter({ incrementBy: 1 }));

// Access current value
console.log(counter.getValue().count);

// Subscribe to changes
const unsubscribe = counter.subscribe(() => {
  console.log("Counter updated:", counter.getValue());
});

// Update props by calling render again
root.render(Counter({ incrementBy: 2 }));

// Cleanup
root.unmount();
unsubscribe();

React Integration

Use resources directly in React components with the useResource hook:

import { useResource } from "@assistant-ui/tap/react";

function MyComponent() {
  const state = useResource(new Counter({ incrementBy: 1 }));
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={state.increment}>Increment</button>
    </div>
  );
}

Design Patterns

Automatic Cleanup

Resources automatically clean up after themselves when unmounted:

const WebSocketResource = resource(() => {
  const [messages, setMessages] = tapState<string[]>([]);

  tapEffect(() => {
    const ws = new WebSocket("ws://localhost:8080");

    ws.onmessage = (event) => {
      setMessages((prev) => [...prev, event.data]);
    };

    // Cleanup happens automatically when resource unmounts
    return () => ws.close();
  }, []);

  return messages;
});

API Wrapper Pattern

A common pattern in assistant-ui is to wrap resource state in a stable API object:

export const tapApi = <TApi extends ApiObject & { getState: () => any }>(
  api: TApi,
) => {
  const ref = tapRef(api);

  tapEffect(() => {
    ref.current = api;
  });

  const apiProxy = tapMemo(
    () =>
      new Proxy<TApi>({} as TApi, new ReadonlyApiHandler(() => ref.current)),
    [],
  );

  return tapMemo(
    () => ({
      state: api.getState(),
      api: apiProxy,
    }),
    [api.getState()],
  );
};

Use Cases

tap is used throughout assistant-ui for:

  1. State Management: Application-wide state without Redux/Zustand
  2. Event Handling: Managing event subscriptions and cleanup
  3. Resource Lifecycle: Auto-cleanup of WebSockets, timers, subscriptions
  4. Composition: Nested resource management (threads, messages, tools)
  5. Context Injection: Passing values through resource boundaries without prop drilling
  6. API Wrapping: Creating reactive API objects with getState() and subscribe()

Example: Tools Management

export const Tools = resource(({ toolkit }: { toolkit?: Toolkit }) => {
  const [state, setState] = tapState<ToolsState>(() => ({
    tools: {},
  }));

  const modelContext = tapModelContext();

  tapEffect(() => {
    if (!toolkit) return;

    // Register tools and setup subscriptions
    const unsubscribes: (() => void)[] = [];
    // ... registration logic

    return () => unsubscribes.forEach((fn) => fn());
  }, [toolkit, modelContext]);

  return tapApi<ToolsApi>({
    getState: () => state,
    setToolUI,
  });
});

Why tap?

  • Reuse React knowledge: Developers already familiar with hooks can immediately work with tap
  • Framework flexibility: Core logic can work outside React components
  • Automatic cleanup: No memory leaks from forgotten unsubscribes
  • Composability: Resources can nest and combine naturally
  • Type safety: Full TypeScript inference for state and APIs
  • Zero dependencies: Lightweight and portable

Comparison with React Hooks

| React Hook | Reactive Resource | Behavior | | ------------- | ----------------- | --------- | | useState | tapState | Identical | | useEffect | tapEffect | Identical | | useMemo | tapMemo | Identical | | useCallback | tapCallback | Identical | | useRef | tapRef | Identical |

License

MIT