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

@dmytromykhailiuk/preact-injectable

v1.0.0

Published

Dependency injection for Preact — bind @dmytromykhailiuk/injectable to the component tree. Hierarchical container modules via context, plus a fully-typed useInject hook. No decorators.

Downloads

81

Readme

@dmytromykhailiuk/preact-injectable

Full documentation: open Docs

Dependency injection for Preact — the @dmytromykhailiuk/injectable container, wired to the component tree.

If you like Angular/NestJS providers but want them in a Preact app, this is that model driven by your JSX: a <Module> owns a DI container for its subtree, nested modules inherit from the ones above them, and a single useInject hook pulls dependencies out — with the exact call signature of container.get. No decorators, no reflect-metadata, no compiler flags.

import { createDIModule, useInject } from "@dmytromykhailiuk/preact-injectable";
import { createInjectionToken } from "@dmytromykhailiuk/injectable";

class Logger {
  log(message: string) {
    console.log(`[log] ${message}`);
  }
}

const API_URL = createInjectionToken<string>("API_URL");

// A module is a component that owns a container for everything inside it.
const AppModule = createDIModule([
  Logger,
  { provide: API_URL, useValue: "https://api.example.com" },
]);

function Greeting() {
  const logger = useInject(Logger); // -> Logger
  const apiUrl = useInject(API_URL); // -> string (inferred from the token)
  logger.log(`hello from ${apiUrl}`);
  return <p>hello</p>;
}

// <AppModule> provides the container; <Greeting> resolves from it.
render(
  <AppModule>
    <Greeting />
  </AppModule>,
  document.body,
);

The whole surface is two things: createDIModule (build a module component) and useInject (resolve inside it). Everything about what you register — class / value / factory / alias providers, multi-providers, injection tokens — comes from @dmytromykhailiuk/injectable and is documented there.


Contents


Install

npm i @dmytromykhailiuk/preact-injectable @dmytromykhailiuk/injectable preact

preact and @dmytromykhailiuk/injectable are peer dependencies — you bring them, this package binds them together. Requires Node 18+ and TypeScript 5.0+. Nothing to enable in tsconfig — no decorators, no reflect-metadata. ESM and CJS builds ship side by side with separate .d.ts / .d.cts declarations, and the package is side-effect-free and tree-shakeable.

import {
  createDIModule, // makes a <Module> component that owns a container
  useInject, // resolves a dependency from the nearest <Module>
} from "@dmytromykhailiuk/preact-injectable";

The provider vocabulary you pass to createDIModulecreateInjectionToken, useValue / useCreate / useExisting, multi, and the inject() you call inside services — all comes straight from @dmytromykhailiuk/injectable. The common types are re-exported here for convenience (Container, ProviderOption, InjectionToken, InjectOptions, Resolver, …).


How it works

There is one shared Preact context that carries the active container down the tree. Each <Module>:

  1. reads the container of the nearest ancestor <Module> from that context;
  2. creates its own container as a child of it (createContainer(parent)) — so resolution is hierarchical;
  3. registers its providers;
  4. provides the container to its descendants;
  5. renders children.

useInject reads the nearest container from the same context and calls container.get(...). Because hierarchy is expressed through nested injectable containers (not nested context objects), a single global useInject works everywhere, and a child module transparently overrides or extends what its parents provide.


createDIModule

createDIModule(providers) takes an array of providers and returns a Module component. Everything rendered inside that component can resolve those providers.

const AuthModule = createDIModule([
  AuthService,
  TokenStore,
  { provide: SESSION, useValue: loadSession() },
]);

<AuthModule>
  <Dashboard />
</AuthModule>;

Providers are registered once, when the module mounts, and each provider is a singleton within that module's container (same instance every time you resolve it). The providers array is anything injectable's register() accepts — a bare class, or a { provide, useValue | useCreate | useExisting, multi? } object. See the injectable providers guide.

The returned component accepts only children. Define it once at module scope (not inside another component's render), so its identity — and its container — stay stable.


useInject

useInject resolves a provider from the nearest <Module>. Its type is Resolverbyte-for-byte identical to container.get:

const logger = useInject(Logger); // class      -> instance
const apiUrl = useInject(API_URL); // token      -> T inferred from the token
const plugins = useInject([Plugin]); // [Class]  -> Plugin[]
const url = useInject<string>("API_URL"); // string -> needs an explicit generic
const maybe = useInject(Analytics, { optional: true }); // -> Analytics | undefined

A token infers its value type with no second generic. A class returns its instance. The [Class] tuple returns an array (multi sugar). { optional: true } widens the return type to include undefined.

Called outside of any <Module>, useInject throws:

useInject must be used within a <Module>. Did you forget to wrap your tree in a component from createDIModule()?

useInject performs a static lookup during render — it resolves against the current container and does not subscribe to later registrations. Pass every provider to createDIModule up front (they are all registered at mount), which is the normal case.


Nested modules

Nest <Module> components and resolution becomes hierarchical: a child checks itself first, then walks up to its parents.

const RootModule = createDIModule([
  Logger,
  { provide: API_URL, useValue: "https://prod.example.com" },
]);

const FeatureModule = createDIModule([
  { provide: API_URL, useValue: "http://localhost:3000" }, // override for this subtree
]);

<RootModule>
  <Header /> {/* useInject(API_URL) -> "https://prod.example.com" */}
  <FeatureModule>
    <Panel /> {/* useInject(API_URL) -> "http://localhost:3000" (child wins) */}
    {/* useInject(Logger) still resolves — inherited from RootModule */}
  </FeatureModule>
</RootModule>;

For multi providers the arrays merge, child values first, then the parents' — the same behaviour injectable gives nested containers. This is how you build per-feature or per-route scopes that inherit the app-wide services above them.


Injection options

useInject forwards injectable's options unchanged:

interface InjectOptions {
  host?: boolean; // resolve only from this module's container, ignore parents
  skipSelf?: boolean; // skip this module, resolve from the parent chain
  multi?: boolean; // treat the result as a multi-provider array
  optional?: boolean; // return undefined instead of resolving to a missing value
}
useInject(Logger, { skipSelf: true }); // explicitly the parent module's Logger
useInject(Config, { host: true }); // only this module's Config

Constructor-style injection with inject()

Inside a service, declare dependencies with injectable's inject() — it resolves against whichever module's container is building the service. No constructor plumbing reaches the component.

import { inject } from "@dmytromykhailiuk/injectable";

class GreetingService {
  private logger = inject(Logger);
  private apiUrl = inject<string>(API_URL);

  greet(name: string) {
    this.logger.log(`hello ${name} via ${this.apiUrl}`);
  }
}

const AppModule = createDIModule([
  Logger,
  GreetingService,
  { provide: API_URL, useValue: "https://api.example.com" },
]);

function Greeter() {
  const greeting = useInject(GreetingService); // its logger + apiUrl already wired
  greeting.greet("world");
  return null;
}

inject() is only valid while a provider is being built (a constructor, a field initializer, or a useCreate factory). From a component, use useInject. See inject() vs container.get().


Lifecycle

A module's container is created when the module mounts and destroyed when it unmountscontainer.destroy() clears every instance, drops subscribers, and detaches from the parent. Remounting a module builds a fresh container (and fresh singletons). This makes <Module> a natural fit for per-route or per-feature scopes that should not leak state across navigations.


Recipes

App root + feature scope

const AppModule = createDIModule([ApiClient, Logger, AuthService]);
const CheckoutModule = createDIModule([CartService, { provide: FLOW, useValue: "checkout" }]);

<AppModule>
  <Shell>
    <CheckoutModule>
      <Checkout />
    </CheckoutModule>
  </Shell>
</AppModule>;

Swap a real service for a fake (Storybook / tests)

const StoryModule = createDIModule([{ provide: Mailer, useValue: new FakeMailer() }]);

<AppModule>
  <StoryModule>
    <OrderForm /> {/* resolves the fake Mailer */}
  </StoryModule>
</AppModule>;

Provide a per-render value

function RequestScope({ req, children }) {
  // Define the module once, outside render, when the providers are static.
  // For a per-value provider, pass it through a token registered at the root.
  return <>{children}</>;
}

API reference

// Build a module component from a provider list.
createDIModule(providers: ProviderOption[]): (props: { children?: ComponentChildren }) => VNode;

// Resolve from the nearest <Module>. Same overloads as injectable's container.get.
const useInject: Resolver;
//   useInject<T>(token: InjectionToken<T>, options?: InjectOptions): T
//   useInject<T>(cls: new () => T, options?: InjectOptions): T
//   useInject<T>(cls: [new () => T], options?: InjectOptions): T[]
//   useInject<T = unknown>(key: string, options?: InjectOptions): T
//   ...with { optional: true } widening the result to T | undefined

// The shared context (advanced interop — read the raw container).
const DIContext: Context<Container | null>;

interface ModuleProps {
  children?: ComponentChildren;
}

The following @dmytromykhailiuk/injectable types are re-exported so you can type providers and tokens without a second import: Container, Resolver, ProviderOption, InjectOptions, OptionalInjectOptions, InjectionToken, Provider, ProviderClass.


Limitations

  • Static resolution. useInject resolves during render and does not re-render on later registrations. Register all providers via createDIModule at mount (the normal case).
  • Inherits injectable's model. Synchronous only, no decorators, no constructor-type injection, and a genuinely missing dependency resolves to undefined rather than throwing. See the injectable limitations.
  • Define modules at module scope. Creating a Module inside another component's render gives it a new identity (and a new container) every render — hoist createDIModule(...) out.

Development

npm run playground   # a runnable Preact demo of nested modules + useInject
npm test             # the full test suite (vitest + @testing-library/preact)
npm run test:coverage
npm run typecheck    # tsc --noEmit
npm run lint         # biome
npm run verify       # lint + typecheck + test + build

License

MIT © Dmytro Mykhailiuk