Skip to main content
Version: 0.1.1

Injectables

An injectable is anything the DI container can hand back when asked. Three kinds:

  1. Classes — the most common. The class is both the token (its identity) and the implementation (its constructor).
  2. Values — pre-built objects, primitives, or configuration. Each one needs an InjectionToken.
  3. Factory results — values produced by a function the container calls the first time the token is requested.

Classes as injectables​

class CartStore {
items = state<Item[]>([]);
}

Any class with a zero-argument constructor works. If the constructor needs dependencies, it gets them via inject():

class CartLogic {
api = inject(ApiClient); // injected before the constructor body runs
cart = inject(CartStore);
}

When CartLogic is constructed, the inject() calls in its field initializers resolve through the active scope.

Auto-registration​

If you inject(SomeClass) and no <Injector> provides SomeClass, the container auto-registers it on the root scope and creates an instance there. So class injectables work with no setup. You don't need a provider for the common case.

If you want per-scope instances of a class, provide it explicitly. <Injector provide={[SomeClass]}> makes that scope create its own copy.

Tokens for non-class injectables​

Configuration strings, primitive values, plain functions — anything that isn't a class — needs an InjectionToken to give it a stable identity:

import { InjectionToken } from '@react-logic/react-logic';

export const API_URL = new InjectionToken<string>('API_URL');

// Provide
<Injector provide={[{ provide: API_URL, useValue: 'https://api.test' }]} />

// Inject
class ApiClient {
url = inject(API_URL);
}

Token identity is by reference. Two new InjectionToken('SAME') calls produce two unrelated tokens. The string is only for debugging.

Lifecycle hooks​

Any injectable can register cleanup with onDestroy:

class TimeService {
timer = setInterval(() => {/*...*/}, 1000);
constructor() {
onDestroy(() => clearInterval(this.timer));
}
}

For services (created via inject()), the callback fires when the providing <Injector> unmounts. Effects created in the constructor — effect(), asyncState() — are tracked automatically and don't need an explicit onDestroy.

Constraints​

A few things to be aware of — none of them mean "can't inject," just "use the right provider shape."

  • No constructor arguments. When you provide a class (useClass or a bare class), the container creates it with new C() — no positional args. Pull what the class needs from inject() field initializers, or use useFactory if you really need to build the value with arguments.
  • Non-classes need a token. Plain values, functions, React components, browser APIs — anything that isn't itself a class — can absolutely be injected, but the lookup key has to be an InjectionToken<T> (you can't use a value as its own key). Pair it with useValue / useFactory. See Providers.
  • inject() runs at construction time only. It's a field initializer or constructor call. Calling inject() from a method body throws — the active scope is only set while the instance is being built.

React components as injectables​

Components are just values, so you can inject them. Useful for slot patterns, theming, or letting a parent scope swap a child's rendering. Since a component doesn't need a logic class to render, pull it in directly with useInjector:

const HeaderComponent = new InjectionToken<FC>('HeaderComponent');

const Page = () => {
const Header = useInjector(HeaderComponent);
return (
<>
<Header />
<main>…</main>
</>
);
};

// Provide at the root, or override in a subtree:
<Injector provide={[{ provide: HeaderComponent, useValue: BrandedHeader }]}>
<Page />
</Injector>

The container doesn't render anything — it just hands you the component reference, and <Page> decides where to put it in its JSX.