Skip to content

Use DI in Server Components

Use createNextDi() when Server Components in one render pass should share scoped dependencies. This guide assumes an App Router project and a server-only composition module.

  1. Create the server composition module

    Pass React’s cache function to the adapter. React owns render memoization; di-craft owns the dependency graph.

    app/di.server.ts
    import "server-only";
    import { cache } from "react";
    import { createNextDi } from "di-craft/next/server";
    export const { getRequestContainer } = createNextDi({
    cache,
    providers,
    requestProviders,
    });
  2. Resolve at the Server Component boundary

    import { getRequestContainer } from "./di.server";
    export default async function Page() {
    const users = await getRequestContainer().get(USERS_SERVICE).list();
    return <UsersView users={users} />;
    }
  3. Reuse the scope in nested Server Components

    Repeated calls to getRequestContainer() return the same child container during the current render pass.

examples/next/nested-server-components.ts
import { createToken, provideFactory, provideValue, Scopes } from "di-craft";
import { createNextDi } from "di-craft/next/server";
type RequestCache = <T>(factory: () => T) => () => T;
// Typed docs use a tiny cache stand-in so this file can be checked without
// installing React. In a real Next app, pass `cache` from "react".
const createRequestCache = (): RequestCache => {
let cached: unknown;
let hasCachedValue = false;
return (factory) => {
return () => {
if (!hasCachedValue) {
cached = factory();
hasCachedValue = true;
}
return cached as ReturnType<typeof factory>;
};
};
};
type RequestContext = {
readonly requestId: string;
};
class UsersService {
private readonly requestContext: RequestContext;
constructor(requestContext: RequestContext) {
this.requestContext = requestContext;
}
list(): readonly string[] {
return ["Ada", "Grace", this.requestContext.requestId];
}
}
const REQUEST_CONTEXT = createToken<RequestContext>("REQUEST_CONTEXT");
const USERS_SERVICE = createToken<UsersService>("USERS_SERVICE");
const { getRequestContainer } = createNextDi({
cache: createRequestCache(),
providers: [
provideFactory(USERS_SERVICE, {
scope: Scopes.Scoped,
deps: { requestContext: REQUEST_CONTEXT },
useFactory: ({ requestContext }) => new UsersService(requestContext),
}),
],
requestProviders: () => [
provideValue(REQUEST_CONTEXT, {
requestId: crypto.randomUUID(),
}),
],
});
type ViewModel = {
readonly title: string;
readonly users: readonly string[];
readonly count: number;
};
const UsersList = async (): Promise<readonly string[]> => {
return getRequestContainer().get(USERS_SERVICE).list();
};
const UsersCount = async (): Promise<number> => {
return getRequestContainer().get(USERS_SERVICE).list().length;
};
const Page = async (): Promise<ViewModel> => {
const [users, count] = await Promise.all([UsersList(), UsersCount()]);
return {
title: "Users",
users,
count,
};
};
await Page();

For Route Handlers, Server Actions, transactions, temporary files, and similar resources, use an explicit request scope.