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

@illuma/react-experimental

v0.4.0

Published

Experimental React adapter library for Illuma's dependency injection. Provides basic signal-based state management and components for DI interop.

Readme

Illuma React (Experimental)

Experimental React adapter for the @illuma/core dependency injection container.

The adapter's job is to make React's tree and the container's tree the same tree: a component subtree gets its own container, resolution walks upwards through React's context, and a container lives exactly as long as the component that provides it. It also ships a one-hook bridge to @illuma/signals so a service's state can drive a render.

Features

  • React bindings – context-based dependency injection for React components
  • Scopes – child containers for a component subtree, disposed with it
  • LifecycleonMount / onUnmount hooks for services that own a resource
  • Signals bridgeuseSignal subscribes a component to a signal
  • Diagnostics – opt-in reporting of providers nothing ever injected
  • Testkit – a container the test owns, with a ready-made wrapper

Installation

yarn add @illuma/react-experimental @illuma/core @illuma/signals

All three peers are required. @illuma/signals is only used by the /signals entry point, but it is declared as a plain peer dependency, so a package manager will ask for it either way.

Requires @illuma/core 2.5.0 or newer: scopes are built on the container's weakParentLink option, so that a container React builds during a render it later throws away can be collected instead of being retained by its parent forever.

Structure

  • @illuma/react-experimental – dependency injection bindings and React integration
  • @illuma/react-experimental/signals – re-exports @illuma/signals and adds the useSignal hook
  • @illuma/react-experimental/testkit – container-backed wrapper for testing components

Example

example/ is a small application that puts every section of this document into one tree — a root container, a scope per screen, a service that owns a subscription, signals rendered through useSignal, a subtree that rebinds one token, and a test that swaps that token for a fake. It builds, runs and tests:

bun run build                      # the package
cd example && npx vite             # http://localhost:5175
cd example && npx vitest run       # 6 tests

Setup

Wrap your application (or a part of it) with IllumaRoot to provide a root dependency injection container.

import type { Provider } from '@illuma/core';
import { IllumaRoot } from '@illuma/react-experimental';
import { Logger, UserService } from './services';

const appProviders: Provider[] = [Logger, UserService];

export const App = () => (
  <IllumaRoot providers={appProviders}>
    <MyComponent />
  </IllumaRoot>
);

Keep that array module-level. providers is read once, when the container is built; a fresh literal on every render would be silently ignored, and the adapter says so in development.

Dependency Injection

A detailed guide on how @illuma/core DI system works can be found in the Docs.

Accessing Dependencies

Use the useDependency hook to resolve services from the container.

import { useDependency } from '@illuma/react-experimental';
import { UserService } from './services';

export const UserProfile = () => {
  const userService = useDependency(UserService);
  
  // Use the service
  return <div>User: {userService.getCurrentUser().name}</div>;
};

Services and other providers live outside of React's render cycle, so they won't cause unnecessary re-renders when their state changes.

You can manipulate services directly from React (or elsewhere) without worrying about render cycles, but if you want to trigger a re-render based on a service's state, you should consider using signals or another state management solution of your choice (like Tanstack, Zustand, Jotai, etc.) in combination with your services.

Component Scopes

You can create a child container for a specific component subtree using ProviderGroup or createComponent. Services provided here will be visible only to children of this component, similar to Angular's component providers.

Option 1: ProviderGroup

import { ProviderGroup } from '@illuma/react-experimental';

export const FeatureSection = () => (
  <ProviderGroup providers={[FeatureService]}>
    <FeatureComponent />
  </ProviderGroup>
);

Option 2: createComponent (HOC)

import { createComponent, useDependency } from '@illuma/react-experimental';

const FeatureComponent = createComponent(() => {
  const service = useDependency(FeatureService);
  return <div>...</div>;
}, [FeatureService]);

Inheritance and Overrides

Child containers inherit all providers from their parents (respecting React's context hierarchy), but you can also override specific providers for a subtree.

import type { Provider } from '@illuma/core';
import { ProviderGroup } from '@illuma/react-experimental';

const providers: Provider[] = [
  { provide: UserService, useClass: MockUserService },
];

export const FeatureSection = () => (
  <ProviderGroup providers={providers}>
    <FeatureComponent />
  </ProviderGroup>
);

export const Dashboard = () => (
  <ProviderGroup providers={[UserService]}>
    <FeatureSection />
    <DashboardComponent />
  </ProviderGroup>
);

In this example, FeatureSection and its children will use MockUserService, while Dashboard and its children (DashboardComponent) will use the original UserService.

Overriding means shadowing in a child container. Listing two providers for one token in the same container is an error, not a last-one-wins — which is worth knowing when you lay out a provider array you also intend to reuse in tests.

Resolution Modifiers

useDependency forwards the container's modifiers.

const maybe = useDependency(OptionalService, { optional: true }); // null if unprovided
const own = useDependency(FeatureService, { self: true });        // this group only
const outer = useDependency(ThemeService, { skipSelf: true });    // start at the parent

optional only covers a token nobody provides. A service whose constructor throws still throws — a broken dependency is a bug, not an absent one.

Writing Services

Two rules matter, and both come from the container rather than from React.

A constructor runs more than once. The container executes each factory once against proxy dependencies to measure the graph, then once for real — and React is free to build a container it later discards, which buys another pair. Under StrictMode a provider's constructor is observed to run three times for the one instance that survives. The number is not a contract; the rule it forces is. A constructor may build fields and inject dependencies, and must cause nothing to happen.

Resources belong to the mount, not to the constructor. React may render a component, build its container, and then discard the whole attempt without ever committing it — and it does exactly that in StrictMode, under Suspense, and whenever a concurrent render is interrupted. A container from a discarded render is collected silently, so anything a constructor had opened would never be closed. Open it on mount instead.

import { makeInjectable } from '@illuma/core';
import { LIFECYCLE_NODE } from '@illuma/react-experimental';
import { signal } from '@illuma/react-experimental/signals';

class _ClockService {
  private _timer?: ReturnType<typeof setInterval>;

  public readonly seconds = signal(0);

  public onMount() {
    this._timer = setInterval(() => this.seconds.update((n) => n + 1), 1000);
  }

  public onUnmount() {
    clearInterval(this._timer);
  }
}

export const ClockService = makeInjectable(_ClockService);

// Register it as a lifecycle node so the group calls the hooks:
<ProviderGroup providers={[ClockService, { provide: LIFECYCLE_NODE, alias: ClockService }]}>

onMount runs when the group commits, onUnmount when it goes away. Hooks fire only for nodes registered in that same container, so a nested group never re-mounts its ancestors'.

A service with hooks therefore needs two entries for one class. Provider arrays nest, so the pair can travel as a single exported constant:

export const clockProviders: Provider = [
  ClockService,
  { provide: LIFECYCLE_NODE, alias: ClockService },
];

How Long a Container Lives

A container lives exactly as long as the component that provides it. Not as long as its effects: React tears effects down whenever a subtree stops being active, and <Activity mode="hidden"> does that while deliberately keeping the subtree's state. A hidden tab keeps its useState, so it keeps its services too — hide one with a half-filled form in it, show it again, and the form is still there.

<Activity mode={visible ? 'visible' : 'hidden'}>
  <ProviderGroup providers={[DraftService]}>
    <Editor />
  </ProviderGroup>
</Activity>

onMount and onUnmount still bracket activity, so they fire on every hide and show — which is what a resource wants. LifecycleRef.beforeDestroy fires when the container is really going away, once the component is gone for good.

Teardown then happens when the collector reaches the discarded scope, so it is prompt but not synchronous. Where that matters — a server request, a test — build the container yourself and destroy it explicitly: <IllumaRoot container={...}> never destroys what it did not create.

One thing to know about a hidden subtree: a signal read through useSignal is served from the last value it announced, and nothing announces while the subscription is detached. The first frame after a reveal can therefore show the value from when the subtree was hidden, corrected on the next commit. Reading the signal directly instead would break React's requirement that two snapshot reads agree, and that costs an infinite render loop rather than one extra frame.

Signals

The reactivity engine is not part of this package. It lives in @illuma/signals, knows nothing about React, and is documented there — signal, computed, linkedSignal, resource, external, untracked and their options.

What this package adds is the bridge, plus a re-export so you only need one import path in React code:

// both work; the second saves you a second dependency in the import list
import { signal, computed } from '@illuma/signals';
import { signal, computed } from '@illuma/react-experimental/signals';

useSignal

Subscribes a component to a signal and re-renders it when the value changes. Built on useSyncExternalStore, with the same read used as the server snapshot, so it is safe under renderToString.

import { useSignal } from '@illuma/react-experimental/signals';
import { useDependency } from '@illuma/react-experimental';

export const Counter = () => {
  const service = useDependency(CounterService);
  const count = useSignal(service.count);
  const double = useSignal(service.double);

  return (
    <div>
      <div>{count} / {double}</div>
      <button onClick={() => service.increment()}>+1</button>
    </div>
  );
};

It takes a signal, not a value, and throws if handed anything else.

Where the state should live

Put the signals on the service and keep derivations there too. React then subscribes to a finished value instead of recomputing one on every render, and the same state is reachable from code that has no component around it.

import { makeInjectable } from '@illuma/core';
import { computed, signal } from '@illuma/signals';

class _CounterService {
  public readonly count = signal(0);
  public readonly double = computed(() => this.count() * 2);

  public increment() {
    this.count.update((c) => c + 1);
  }
}

export type CounterService = _CounterService;
export const CounterService = makeInjectable(_CounterService);

Creating signals in a constructor or a field initializer is fine: they are values, not effects, and the copy built during the container's measuring pass is simply discarded.

Server Rendering

Effects never run on a server, so nothing there can own a container's lifetime. Build one per request and hand it to IllumaRoot, which then only publishes it — never bootstraps, rebuilds, or destroys it.

const container = new NodeContainer({ instant: false });
container.provide(requestProviders);
container.bootstrap();

try {
  return renderToString(
    <IllumaRoot container={container}>
      <App />
    </IllumaRoot>,
  );
} finally {
  container.destroy();
}

No mount happens, so no onMount does either: a server render resolves services but takes none of the resources they own.

Testing

createTestScope builds a container the test owns, and returns a wrapper for @testing-library/react. Swapping one token restages the whole graph beneath it.

import { createTestScope } from '@illuma/react-experimental/testkit';

const scope = createTestScope({
  providers: [{ provide: ApiService, useClass: FakeApi }],
});

render(<TodoList />, { wrapper: scope.wrapper });

expect(scope.container.get(ApiService)).toBeInstanceOf(FakeApi);
scope.destroy();

The container outlives the tree, so it can still be inspected after an unmount.

There is no overrideProvider: the test scope is a root container, and a container rejects a second provider for a token it already has. Spreading your application's whole provider list and appending an override will throw. Split the list instead, so the bindings a test replaces are not in the part it reuses:

export const appProviders: Provider[] = [Logger, UserService];
export const platformProviders: Provider[] = [{ provide: ApiService, useClass: HttpApi }];
export const rootProviders: Provider[] = [appProviders, platformProviders];
const scope = createTestScope({
  providers: [appProviders, { provide: ApiService, useClass: FakeApi }],
});

Diagnostics

Opt in during development to be told which providers nothing ever injected.

import { enableReactDiagnostics } from '@illuma/react-experimental';

if (import.meta.env.DEV) enableReactDiagnostics();

Output goes through Illuma.setLogger, sharing one control surface with the core's own diagnostics. It is a no-op in production builds.