React
awilixify/react brings the module model to React applications.
React modules register two kinds of things:
providers: services, clients, stores, repositories, and other non-UI objectscomponents: React components that should receive system-injected deps
Components receive a deps prop from ReactDIContext. Application code renders those components with their own public props only.
For the motivation behind frontend DI, see Frontend Overview.
Install
react is an optional peer dependency of awilixify.
npm install awilix awilixify react react-domDependency Surfaces
React modules infer two dependency maps from the same module definition.
InferProviderDeps<T> is for providers:
- local providers
- exported providers from imported modules
- global dependencies
InferComponentDeps<T> is for components:
- everything from provider deps
- local components
- exported components from imported modules
The split is intentional. Providers should not depend on React components. Components can compose services and other components.
// projects/projects.module.ts
import {
createModule,
type InferComponentDeps,
type InferProviderDeps,
type ModuleDef,
} from "awilixify/react";
import { ProjectsPage } from "./components/ProjectsPage";
const components = {
ProjectsPage,
};
export type ProjectsModuleDef = ModuleDef<{
providers: {
projectService: ProjectService;
projectSummaryService: ProjectSummaryService;
};
components: typeof components;
componentExportKeys: ["ProjectsPage"];
}>;
// all deps including components, used for components
export interface Deps extends InferComponentDeps<ProjectsModuleDef> {}
// all deps without components, used for providers
export interface ProviderDeps extends InferProviderDeps<ProjectsModuleDef> {}Inside the module, Deps is the type used by React components. ProviderDeps is the type used by services and other providers.
Providers
Providers are normal awilixify providers. They should depend on other providers, not on components.
// projects/project.service.ts
import type { Project } from "./project.types";
const projects: Project[] = [
{ id: "billing", name: "Billing API", status: "active", owner: "Platform" },
{ id: "checkout", name: "Checkout UI", status: "active", owner: "Commerce" },
{ id: "reporting", name: "Reporting", status: "paused", owner: "Data" },
];
export class ProjectService {
list(): Project[] {
return projects;
}
get(id: string): Project | undefined {
return projects.find((project) => project.id === id);
}
}// projects/project-summary.service.ts
import type { ProviderDeps } from "./projects.module";
export class ProjectSummaryService {
constructor(
private readonly projectService: ProviderDeps["projectService"],
) {}
getSummary() {
const projects = this.projectService.list();
return {
total: projects.length,
active: projects.filter((project) => project.status === "active").length,
paused: projects.filter((project) => project.status === "paused").length,
};
}
}ProviderDeps["projectService"] is inferred from the module definition. It does not include component keys like ProjectPage or StatusBadge.
Components
Use WithDeps<TProps, TDeps> when a component has public props.
// projects/components/StatusBadge.tsx
import type { WithDeps } from "awilixify/react";
import type { Project } from "../project.types";
import type { Deps } from "../projects.module";
export type StatusBadgeProps = {
status: Project["status"];
};
export function StatusBadge({ status }: WithDeps<StatusBadgeProps, Deps>) {
return <span>{status}</span>;
}// projects/components/ProjectPage.tsx
import type { WithDeps } from "awilixify/react";
import type { Deps } from "../projects.module";
export type ProjectPageProps = {
projectId: string;
};
export function ProjectPage({
projectId,
deps,
}: WithDeps<ProjectPageProps, Deps>) {
const project = deps.projectService.get(projectId);
if (!project) {
return <p>Project not found.</p>;
}
return (
<section>
<h2>{project.name}</h2>
<deps.StatusBadge status={project.status} />
</section>
);
}Use WithDepsOnly<TDeps> when a component has no public props.
// projects/components/ProjectsPage.tsx
import type { WithDepsOnly } from "awilixify/react";
import type { Deps } from "../projects.module";
export function ProjectsPage({ deps }: WithDepsOnly<Deps>) {
const projects = deps.projectService.list();
const summary = deps.projectSummaryService.getSummary();
return (
<section>
<h2>Projects</h2>
<p>
{summary.active} active / {summary.paused} paused
</p>
{projects.map((project) => (
<deps.ProjectRow key={project.id} project={project} />
))}
</section>
);
}deps is owned by the system. It is injected when the component is resolved from the module scope, and it cannot be supplied from JSX.
When a component is rendered from deps, only its public props are accepted. The deps prop is removed from the public component type.
Module Definition
Components are registered separately from providers.
componentExports exposes selected components to modules that import this module. It is usually used for pages, route targets, or reusable UI pieces that another module should be allowed to render.
// projects/projects.module.ts
import {
createModule,
type InferComponentDeps,
type InferProviderDeps,
type ModuleDef,
} from "awilixify/react";
import { ProjectPage } from "./components/ProjectPage";
import { ProjectRow } from "./components/ProjectRow";
import { ProjectsPage } from "./components/ProjectsPage";
import { StatusBadge } from "./components/StatusBadge";
import { ProjectService } from "./project.service";
import { ProjectSummaryService } from "./project-summary.service";
const components = {
ProjectsPage,
ProjectPage,
ProjectRow,
StatusBadge,
};
export type ProjectsModuleDef = ModuleDef<{
providers: {
projectService: ProjectService;
projectSummaryService: ProjectSummaryService;
};
components: typeof components;
componentExportKeys: ["ProjectsPage", "ProjectPage"];
}>;
export interface Deps extends InferComponentDeps<ProjectsModuleDef> {}
export interface ProviderDeps extends InferProviderDeps<ProjectsModuleDef> {}
export const ProjectsModule = createModule<ProjectsModuleDef>({
name: "ProjectsModule",
providers: {
projectService: ProjectService,
projectSummaryService: ProjectSummaryService,
},
components,
componentExports: ["ProjectsPage", "ProjectPage"],
});Importing Components
Imported component exports become part of the importing module's component deps. They do not become part of provider deps.
// app/AppShell.tsx
import type { WithDepsOnly } from "awilixify/react";
import type { Deps } from "./app.module";
export function AppShell({ deps }: WithDepsOnly<Deps>) {
return (
<main>
<h1>Projects</h1>
<deps.ProjectsPage />
</main>
);
}// app/app.module.ts
import {
createModule,
type InferComponentDeps,
type ModuleDef,
} from "awilixify/react";
import { ProjectsModule } from "../projects/projects.module";
import { AppShell } from "./AppShell";
const components = {
AppShell,
};
export type AppModuleDef = ModuleDef<{
imports: [typeof ProjectsModule];
components: typeof components;
}>;
export interface Deps extends InferComponentDeps<AppModuleDef> {}
export const AppModule = createModule<AppModuleDef>({
name: "AppModule",
imports: [ProjectsModule],
components,
});Bootstrapping
Use ReactDIContext.create(...) to build the module graph. Components are registered in the Awilix scope and can be resolved by key.
import { ReactDIContext, type WithoutDeps } from "awilixify/react";
import { createRoot } from "react-dom/client";
import type { AppShell } from "./app/AppShell";
import { AppModule } from "./app/app.module";
const app = ReactDIContext.create(AppModule);
const AppShellComponent =
app.scope.resolve<WithoutDeps<typeof AppShell>>("AppShell");
createRoot(document.getElementById("root")!).render(<AppShellComponent />);For components with public props, WithoutDeps<T> gives the renderable public component type.
import { ReactDIContext, type WithoutDeps } from "awilixify/react";
import type { ProjectPage } from "./projects/components/ProjectPage";
const app = ReactDIContext.create(AppModule);
const ProjectPageComponent =
app.scope.resolve<WithoutDeps<typeof ProjectPage>>("ProjectPage");
<ProjectPageComponent projectId="billing" />;Routing
Routers usually need React components, so router construction belongs in React composition code rather than in providers.
With TanStack Router, keep the route tree in a normal factory function so the router type can still be inferred:
import {
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
export function createAppRouter({ AppShell, ProjectsPage, ProjectPage }: Deps) {
const rootRoute = createRootRoute({ component: AppShell });
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: ProjectsPage,
});
const projectRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/projects/$projectId",
component: ProjectPage,
});
return createRouter({
routeTree: rootRoute.addChildren([indexRoute, projectRoute]),
});
}
export type AppRouter = ReturnType<typeof createAppRouter>;Then create the router once in the root component.
import { RouterProvider } from "@tanstack/react-router";
import { useRef } from "react";
import type { WithDepsOnly } from "awilixify/react";
import type { Deps } from "./app.module";
import { type AppRouter, createAppRouter } from "./app-router";
export function App({ deps }: WithDepsOnly<Deps>) {
const routerRef = useRef<AppRouter | null>(null);
if (!routerRef.current) {
routerRef.current = createAppRouter({
AppShell: deps.AppShell,
ProjectsPage: deps.ProjectsPage,
ProjectPage: deps.ProjectPage,
});
}
return <RouterProvider router={routerRef.current} />;
}Notes
- Backend-only features like controllers, handlers, pre-handlers, initializers, and interceptors are not part of the React API.
- Components receive dependencies through
props.deps. - Providers should stay UI-independent and use
InferProviderDeps<T>. - Components use
InferComponentDeps<T>and can render components fromdeps.
Example
Full working example:
