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

@baby-yak/service-loom-react

v1.0.3

Published

React hooks and utilities for @baby-yak/service-loom-js

Downloads

265

Readme

@baby-yak/service-loom-react

[!IMPORTANT] Beta - API is stable but the package is still early. Feedback welcome.

React hooks for @baby-yak/service-loom-js — connect your services to components with minimal boilerplate.

Install

npm install @baby-yak/service-loom-react

Requires react >= 17 and @baby-yak/service-loom-js as peer dependencies.

Quick start

Services and modules live outside React — create them once, export, and consume anywhere with hooks. No providers needed for the common case.

// services.ts — create once, outside React
const app = createModule({
  counter: new CounterService(),
  server: new ServerService(),
});

app.start(); // void — errors go to module.events.on('errorStarting', ...)

export const services = app.services;
// Counter.tsx — consume with hooks
import { services } from './services';

function Counter() {
  const count = useReactiveState(services.counter, (s) => s.count);
  const increment = useAction(services.counter, 'increment');

  return <button onClick={increment}>{count}</button>;
}

For localizing services to a subtree of the component tree, see Context Providers below.


Hooks

| Hook | Description | | ------------------ | --------------------------------------------------------------- | | useReactiveState | Subscribe to service state, re-renders on change | | useEvent | Subscribe to a service event for the component lifetime | | useAction | Get a typed action function from a service | | useActionAsync | Track async action execution — loading, result, and error state | | useStateEffect | Run a side effect on state change, without re-rendering |


useReactiveState

Re-renders the component whenever the state changes. Accepts a StateClient or a ServiceClient with state.

// whole state
const state = useReactiveState(services.counter);

// with selector — only re-renders when the selected value changes
const count = useReactiveState(services.counter, (s) => s.count);

Both forms accept an optional deps array (same semantics as useEffect) to control when the subscription is re-created:

const value = useReactiveState(services.counter, (s) => s.count, []);

useEvent

Subscribes to a service event and calls the listener whenever it fires. The subscription is set up on mount and torn down on unmount.

useEvent(services.server, 'connected', () => {
  console.log('server connected');
});

Pass a deps array to re-create the subscription when a dependency changes. Include any values the listener closes over:

useEvent(services.server, 'connected', () => console.log(`connected as ${userId}`), [userId]);

useAction

Returns a typed action function. Equivalent to services.myService.actions.someAction — a convenience wrapper for uniform hook-style access. Does not subscribe to anything — no re-render, no cleanup.

const increment = useAction(services.counter, 'increment');
increment();

useActionAsync

Tracks the async execution of an action — loading state, result, and error. Previous data is preserved while loading and on error, replaced only on success. Stale results from a previous call are discarded if execute is called again before it resolves.

const {
  execute: addItem,
  data,
  isLoading,
  isError,
  error,
} = useActionAsync(services.db, 'addItem');
addItem('new item');

Also accepts a raw function directly:

const {
  execute: fetchUser,
  data: user,
  isLoading,
} = useActionAsync((id: string) => fetch(`/api/users/${id}`).then((r) => r.json()));

Return shape:

| Field | Type | Description | | ----------- | ---------------- | ------------------------------------ | | execute | function | Call to trigger the action | | data | T \| undefined | Last successful result | | isLoading | boolean | true while the action is in flight | | isError | boolean | true if the last call threw | | error | unknown | The thrown error, if any |


useStateEffect

Runs a side effect whenever state changes — without causing a re-render. Useful for analytics, logging, syncing to external systems.

The callback receives (state, prev) — also called once immediately on mount with prev = undefined. Guard against it if you only want changes:

useStateEffect(services.counter, (state, prev) => {
  if (prev === undefined) return; // skip initial call
  analytics.track('count_changed', { from: prev.count, to: state.count });
});

With a selector — only fires when the selected slice changes:

useStateEffect(
  services.counter,
  (s) => s.count,
  (count, prev) => console.log(`count: ${prev} → ${count}`),
);

Pass a deps array to control when the subscription is re-created:

useStateEffect(services.counter, (state) => doSomething(state, userId), [userId]);

Working with ServiceClient

All hooks accept either a dedicated client (StateClient, EventClient, ActionClient) or a ServiceClient directly:

useReactiveState(services.counter); // ServiceClient
useReactiveState(services.counter.state); // StateClient directly

Context Providers

For cases where services need to be localized to a subtree — multiple independent instances of the same service, per-route state, feature isolation — use the context factory helpers.

Each call to createModuleContext / createServiceContext creates an isolated context instance, so multiple providers in the same tree don't interfere.

[!NOTE] Caveats

  • Service lifecycle is tied to the React tree — start() is called on mount, stop() on unmount. If this isn't what you want, create the module outside React instead (see Quick start).
  • In React's Strict Mode (development only), components intentionally mount → unmount → remount. The providers handle this correctly — the full lifecycle runs twice in sequence.
  • Lifecycle errors inside the provider are caught and logged via module.events.on('errorStarting' / 'errorStopping'). Register a listener on the module before passing it to the provider if you need custom error handling.

| Factory | Description | | ---------------------- | -------------------------------------------- | | createModuleContext | Scoped Provider + hook for a set of services | | createServiceContext | Scoped Provider + hook for a single service |

createModuleContext

type MyModule = {
  counter: ICounter;
  server: IServer;
};

// create once and export - fully typed provider and hook
export const {
  ModuleProvider, // the <Provider/> component
  useModule, // hook to get the module in the consumers
} = createModuleContext<MyModule>();
// provide — services created once on mount, lifecycle managed automatically
<ModuleProvider
  createModule={() => ({
    counter: new CounterService(),
    server: new ServerService(),
  })}
>
  <App />
</ModuleProvider>
// consume anywhere in the subtree — returns ModuleClient (fully typed, no casting)
const { services, state, events } = useModule();
const { counter, server } = services;

// reactive lifecycle state:
const { isStarted } = useReactiveState(state);

// lifecycle events:
useEvent(events, 'started', () => console.log('ready'));

createServiceContext

Same pattern for a single service:

export const {
  ServiceProvider, // the <Provider/>
  useService, // the hook for consumers
} = createServiceContext<CounterService>();
<ServiceProvider createService={() => new CounterService()}>
  <CounterView />
</ServiceProvider>
//in consumers:
const counter = useService();
counter.actions.increment();

License

MIT