Dependency injection adapters
The DI runtime is pluggable. The framework defines a contract called the DI adapter. The rest of the library only talks to that contract, never to a specific implementation.
That means you can replace the default DI runtime with another one. The first alternative is @react-logic/angular-adapter, which backs react-logic with Angular's EnvironmentInjector so you can share Angular services.
The contract
The full shape lives at DIAdapter. Six operations:
createScope(providers, parent)— build a child scope.disposeScope(scope)— tear it down.runIn(scope, fn)— set the active scope whilefnruns.construct(scope, fn)— same asrunIn, plus collectonDestroycallbacks and track which services were injected (used byuseLogic).inject(token, optional?)— resolve a token through the active scope.onDestroy(fn)— register cleanup tied to whatever's currently being constructed.
Plus a rootScope property: the top-level scope used when no <Injector> wraps the React tree.
The default adapter
The default adapter is registered automatically when you import @react-logic/di. It's a small purpose-built DI implementation with parent-delegating scopes, instance caching, lifecycle hooks, and hierarchical resolution. Most apps never need to think about it.
import '@react-logic/di'; // registers the default adapter as a side effect
Swapping adapters
Call setDIAdapter(adapter) once, before mounting React. Calling it later won't migrate instances that are already constructed.
import { setDIAdapter } from '@react-logic/react-logic';
import { createAngularAdapter } from '@react-logic/angular-adapter';
import { createEnvironmentInjector, Injector } from '@angular/core';
const root = createEnvironmentInjector([], Injector.NULL as never);
setDIAdapter(createAngularAdapter(root));
// …then mount React.
After the swap, inject(), <Injector>, useLogic, and onDestroy all delegate to the Angular adapter. Logic classes don't change. The imports they use are the same.
What's portable
Code that only touches inject, onDestroy, and <Injector> from @react-logic/di runs unchanged on either adapter. The adapter swap is invisible to logic classes.
What's not portable:
- Adapter-specific provider shapes. Angular's full provider system has flags like
multi: trueandfactorywith a deps array that the default adapter doesn't model. If you use those, you're tied to Angular. - Adapter-specific lifecycle. Angular's
DestroyRefcascades along its injector tree. The default adapter has its own model. The framework'sonDestroyhides the difference for the common case. - Custom typedoc options or build settings tied to one adapter's runtime.
Writing your own adapter
Any DI runtime that supports a scope tree, parent delegation, cached instances, and lifecycle hooks can host react-logic. The pattern:
- Pick a scope type (your library's "injector" or equivalent).
- Implement the six operations and
rootScope. - For
construct, create a short-lived destroy context. It needs to track user-levelonDestroycalls separately from service-internal ones, and to track every valueinject()returns. - Pass the assembled object to
setDIAdapter.
See angular-adapter.ts for a working ~70-line reference implementation.
Why an adapter at all?
Two reasons:
- Interop. Some teams have existing Angular (or other-framework) services they want to share with React. The adapter is the bridge.
- Replaceability. The default adapter is small but opinionated. If you want stricter behavior — no class auto-registration, multi-providers, or destroy semantics that match your test framework — an adapter lets you swap it without forking the library.
If you don't need either, you'll never touch this layer.