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

@blac/react

v2.0.19

Published

React bindings for BlaC — useBloc hook with automatic re-render optimization

Readme

@blac/react

React bindings for BlaC — useBloc hook with proxy-based automatic re-render optimization.

Documentation · npm

[!WARNING] BlaC v2 is in pre-release (beta). While in beta, breaking API changes may ship in patch releases without a major version bump. Pin an exact version and check the changelog before upgrading. Strict semver resumes once v2 is officially out of beta.

Installation

pnpm add @blac/react @blac/core

Requires React 18+.

Quick Start

import { Cubit } from '@blac/core';
import { useBloc } from '@blac/react';

class CounterCubit extends Cubit<{ count: number }> {
  constructor() {
    super({ count: 0 });
  }
  increment = () => this.emit({ count: this.state.count + 1 });
  decrement = () => this.emit({ count: this.state.count - 1 });
}

function Counter() {
  const [state, counter] = useBloc(CounterCubit);
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={counter.increment}>+</button>
      <button onClick={counter.decrement}>-</button>
    </div>
  );
}

Reactivity model

BlaC follows one rule:

A component becomes reactive only to the paths it reads during its own render.

useBloc returns a proxy that records which paths you read; the component re-renders only when one of those paths changes. Reads outside render — effects, event handlers, setTimeout, await — record nothing.

Props are snapshots — untracked()

A tracking proxy stays bound to the component that created it. If a parent passes a proxied value to a child and the child reads from it, that read is attributed to the parent — so the parent over-subscribes and re-renders for paths only the child uses.

Pass the value through untracked() to hand the child a detached snapshot instead:

import { useBloc, untracked } from '@blac/react';

function List() {
  const [state] = useBloc(TodoBloc);
  // The parent reads each id (for keys). Rows read `.label` etc. off the
  // snapshot, which is detached — those reads are NOT attributed to List.
  return (
    <ul>
      {state.items.map((item) => (
        <Row key={item.id} item={untracked(item)} />
      ))}
    </ul>
  );
}

The trade-off is intentional and React-like: a component that receives an untracked() value is not reactive to it — a change triggers nothing, memo or not. If no component reads a path during its own render, a change to that path re-renders nothing. To react to data, a component must be a consumer itself (call its own useBloc). Nobody up the tree re-renders for a path unless they read it themselves.

Making this automatic — per-component read scoping so untracked() isn't needed by hand — is a planned compiler/Babel plugin. See plans/blac-ambient-tracking/design.md.

useBloc

const [state, bloc, ref] = useBloc(MyBloc, options?);

Returns: [state, bloc, ref]

  • state — current state snapshot (proxied for auto-tracking)
  • bloc — bloc instance for calling methods
  • ref — internal component ref (advanced usage)

The Three Input Lanes

BlaC blocs receive external data through three distinct channels:

| Lane | Purpose | Keying | Lifetime | Example | | ---------- | ---------------------------------------------- | ----------------------------------------- | ------------------------- | ----------------------------------------- | | args | Typed creation data; derives instance identity | Yes (structural hash or static key) | Once at init() | userId, endpoint | | deps | Non-serializable refs, callbacks, handles | Never | Live, per-consumer merged | ref, onComplete callback, emblaApi | | events | Values that change over time or are late-bound | N/A | Called from effects | cubit.slidesChanged(v) from useEffect |

args: Typed Construction Data

When a bloc declares Args, you must pass them at the call site. Args are fed to the bloc's init(args) method before the first state snapshot, and they derive the instance identity by default.

import { Cubit } from '@blac/core';
import { useBloc } from '@blac/react';

class UserCardCubit extends Cubit<UserCardState, { userId: string }> {
  // Constructor is zero-arg; framework calls init(args) before first snapshot
  init(args: { userId: string }) {
    this.userId = args.userId;
    void this.loadUser(args.userId);
  }
}

function UserCard({ userId }: { userId: string }) {
  // args is required and type-checked when Args != void
  const [state, cubit] = useBloc(UserCardCubit, { args: { userId } });
  return <div>{state.user?.name}</div>;
}

Key properties:

  • Required when declared — omitting args or passing the wrong shape is a type error.
  • Drives identity — different args ⇒ different instance. Same args ⇒ same instance (dev-warn if args mismatch on same-keyed second call).
  • Serializable only — non-serializable values (refs, callbacks) belong in the deps lane (below).
  • Per-component private instances — embed a per-mount unique ID inside args (using React's useId()) to give each mount its own instance, disposed on unmount:
const id = useId();
const [state, cubit] = useBloc(FormCubit, { args: { ...options, _id: id } });

deps: Non-Serializable Refs and Callbacks

Use deps to inject refs, stable callbacks, and long-lived controller handles. Unlike args, deps are:

  • Never keying — different refs don't fork the instance
  • Per-consumer merged — each component contributes its own slice
  • Read lazily — accessed via this.deps.x when needed, may be undefined
  • Live — can change over time

deps is not a useBloc option. A component contributes its slice from a mount effect, using APPLY_DEPS / REMOVE_DEPS_OWNER from @blac/core (marked @internal today; a friendlier wrapper may land later):

import { useEffect, useId, useRef } from 'react';
import { APPLY_DEPS, REMOVE_DEPS_OWNER } from '@blac/core';
import { useBloc } from '@blac/react';

const inputRef = useRef<HTMLInputElement>(null);
const ownerId = useId();
const [state, cubit] = useBloc(FileUploadCubit, { args: { endpoint } });

useEffect(() => {
  cubit[APPLY_DEPS](ownerId, { inputRef });
  return () => cubit[REMOVE_DEPS_OWNER](ownerId);
}, [cubit, inputRef, ownerId]);

The bloc reads them lazily and guards for absence:

class FileUploadCubit extends Cubit<
  UploadState,
  { endpoint: string },
  {
    inputRef?: RefObject<HTMLInputElement>;
    onComplete?: () => void;
  }
> {
  async upload() {
    this.deps.inputRef?.current?.click?.();
    // ... perform upload ...
    this.deps.onComplete?.();
  }
}

Multi-consumer merge: when multiple components provide the same cubit with different deps, their keys are shallow-merged:

// Component A owns inputRef
cubitA[APPLY_DEPS](ownerIdA, { inputRef });

// Component B owns onSubmit
cubitB[APPLY_DEPS](ownerIdB, { onSubmit });

// cubit.deps === { inputRef, onSubmit } (merged from both consumers)

Avoid raw callbacks — the callback staleness gotcha. Prefer:

  1. Don't inject callbacks — invert (best): expose state and let React call the fresh callback in its own effect.
  2. Stabilize at source — wrap in useCallback.
  3. As an event — push the callback via a bloc method called from your effect.

events: Methods Called from Effects

For data that changes over the instance's life (a slides array, a theme selection), call an ordinary bloc method from an effect — not a provider-owned input. This keeps ownership explicit and eliminates render-time mutation.

class CarouselCubit extends Cubit<CarouselState> {
  slidesChanged(slides: Slide[]) {
    this.patch({ slides, total: slides.length });
  }
}
function Carousel({ slides }: { slides: Slide[] }) {
  const [state, cubit] = useBloc(CarouselCubit, { args: { id } });

  // ONE component owns syncing this live value
  useEffect(() => {
    cubit.slidesChanged(slides);
  }, [cubit, slides]);

  return /* ... */;
}

Convention: one component owns syncing any given live value. Two components calling the same event from both their effects on the same shared instance is a design smell (and rare, because keyed args usually route multi-consumer cases to distinct instances).

Tracking Modes

Auto-tracking (default): Only re-renders when accessed properties change.

const [state] = useBloc(UserBloc);
return <h1>{state.name}</h1>; // only re-renders when name changes

Manual dependencies (select): Explicit dependency array (disables auto-tracking).

const [state] = useBloc(CounterCubit, {
  select: (state) => [state.count],
});

Options

| Option | Type | Description | | ----------- | ---------------------------- | -------------------------------------------------------------------------------- | | args | Args type | Required when bloc declares Args != void; forbidden when void | | select | (state, bloc) => unknown[] | Manual dependency selector (renamed from dependencies); disables auto-tracking | | onMount | (bloc) => void | Called when component mounts | | onUnmount | (bloc) => void | Called when component unmounts |

Auto-tracking is always on when select is omitted — it is not a configurable option. deps, autoInstance, and instanceId are not useBloc options: wire deps from a mount effect (APPLY_DEPS / REMOVE_DEPS_OWNER); for per-mount private instances, embed a stable unique ID in args (e.g. { args: { _id: useId() } }).

Identity and Keying

Instance identity is resolved in precedence order:

  1. <BlocProvider> context id — inherited from an ancestor provider
  2. static key(args) → structural hash of args — default when the bloc declares Args (static key wins if defined; otherwise a stable hash of all args)
  3. 'default' — singleton fallback

For a per-mount private instance, embed a stable unique ID inside args so each mount hashes to a distinct key:

const id = useId();
useBloc(FormCubit, { args: { ...options, _id: id } });

Blocs declare explicit identity via a static class property:

class DocumentCubit extends Cubit<
  DocState,
  { docId: string; readonly: boolean }
> {
  static key = (args) => args.docId;
  // Identity is docId; readonly config rides along but doesn't fork instances
}

Instance Sharing and Lifecycle

By default, all components using useBloc(MyBloc) with the same identity share one instance. For per-component private instances, embed a unique ID in args so each mount derives a distinct key:

// All users with userId=123 share one instance
useBloc(UserCardCubit, { args: { userId: 123 } });

// Each component mount gets its own instance, disposed on unmount
const id = useId();
useBloc(FormCubit, { args: { ...options, _id: id } });

// Explicit stable key (escape hatch for non-derivable identity)
useBloc(EditorCubit, { args: { _id: 'editor-1' } });

Breaking Changes (v2)

  • dependencies option renamed to select — avoids confusion with the deps (non-serializable handles) lane.
  • autoTrack, autoInstance, instanceId, and deps are no longer useBloc options — auto-tracking is always on (opt out per-consumer with select); per-mount private instances embed a unique ID in args; deps are wired from a mount effect via APPLY_DEPS / REMOVE_DEPS_OWNER.
  • Zero-arg constructor + init(args) lifecycle — all blocs now use new Type() with no constructor args. Blocs that declare Args receive them via init(args) called by the framework before the first state snapshot.
  • args is required when declared, forbidden when void — enforced by the type system; no runtime guard needed.

Configuration

import { configureBlacReact } from '@blac/react';

// Configuration is currently empty; the tracking model is fixed and not configurable.
configureBlacReact({});

Testing

import { renderWithBloc } from '@blac/react/testing';

See the testing docs for details.

License

MIT