Next.js Integration
This guide shows how to integrate ts-ioc-container with Next.js applications, covering both App Router (Server Components) and Pages Router patterns, as well as client-side usage.
Overview
Next.js has unique requirements due to its hybrid rendering model:
- Server Components - Run on the server, can use container directly
- Client Components - Run in browser, need React context
- API Routes - Server-side, request-scoped containers
- Middleware - Edge runtime considerations
Scope Hierarchy in Next.js
- Application scope - Singleton services (shared across requests on server)
- Request scope - Per-request services (API routes, server actions)
- Page scope - Per-page client services
- Widget scope - Per-component client services
Server-Side Setup
Create the server-side container configuration:
TypeScript src/di/server-container.ts
// src/di/server-container.ts
import 'reflect-metadata';
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
} from 'ts-ioc-container';
import type { IContainer } from 'ts-ioc-container';
import { DatabasePool } from '../services/DatabasePool';
import { CacheService } from '../services/CacheService';
import { UserRepository } from '../repositories/UserRepository';
import { RequestContext } from '../services/RequestContext';
// Singleton container for server-side
let serverContainer: IContainer | null = null;
export function getServerContainer(): IContainer {
if (!serverContainer) {
serverContainer = new Container(new MetadataInjector(), { tags: ['application'] })
.addRegistration(
// Application-scoped services (singleton across all requests)
Registration.fromClass(DatabasePool)
.pipe(singleton())
.to('IDatabasePool'),
Registration.fromClass(CacheService)
.pipe(singleton())
.to('ICacheService'),
)
.addRegistration(
// Request-scoped services
Registration.fromClass(UserRepository)
.pipe(scope((s) => s.hasTag('request')))
.pipe(singleton())
.to('IUserRepository'),
Registration.fromClass(RequestContext)
.pipe(scope((s) => s.hasTag('request')))
.pipe(singleton())
.to('IRequestContext'),
);
}
return serverContainer;
}
export function createRequestScope(): IContainer {
return getServerContainer().createScope({ tags: ['request'] });
}// src/di/server-container.ts
import 'reflect-metadata';
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
} from 'ts-ioc-container';
import type { IContainer } from 'ts-ioc-container';
import { DatabasePool } from '../services/DatabasePool';
import { CacheService } from '../services/CacheService';
import { UserRepository } from '../repositories/UserRepository';
import { RequestContext } from '../services/RequestContext';
// Singleton container for server-side
let serverContainer: IContainer | null = null;
export function getServerContainer(): IContainer {
if (!serverContainer) {
serverContainer = new Container(new MetadataInjector(), { tags: ['application'] })
.addRegistration(
// Application-scoped services (singleton across all requests)
Registration.fromClass(DatabasePool)
.pipe(singleton())
.to('IDatabasePool'),
Registration.fromClass(CacheService)
.pipe(singleton())
.to('ICacheService'),
)
.addRegistration(
// Request-scoped services
Registration.fromClass(UserRepository)
.pipe(scope((s) => s.hasTag('request')))
.pipe(singleton())
.to('IUserRepository'),
Registration.fromClass(RequestContext)
.pipe(scope((s) => s.hasTag('request')))
.pipe(singleton())
.to('IRequestContext'),
);
}
return serverContainer;
}
export function createRequestScope(): IContainer {
return getServerContainer().createScope({ tags: ['request'] });
}API Routes (App Router)
Use request-scoped containers in API route handlers:
TypeScript src/app/api/users/route.ts
// src/app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
import type { IRequestContext } from '@/services/RequestContext';
export async function GET(request: NextRequest) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const context = scope.resolve<IRequestContext>('IRequestContext');
// Set request context
context.setRequestId(crypto.randomUUID());
const users = await userRepo.findAll();
return NextResponse.json(users);
} finally {
scope.dispose();
}
}
export async function POST(request: NextRequest) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const body = await request.json();
const user = await userRepo.create(body);
return NextResponse.json(user, { status: 201 });
} finally {
scope.dispose();
}
}// src/app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
import type { IRequestContext } from '@/services/RequestContext';
export async function GET(request: NextRequest) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const context = scope.resolve<IRequestContext>('IRequestContext');
// Set request context
context.setRequestId(crypto.randomUUID());
const users = await userRepo.findAll();
return NextResponse.json(users);
} finally {
scope.dispose();
}
}
export async function POST(request: NextRequest) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const body = await request.json();
const user = await userRepo.create(body);
return NextResponse.json(user, { status: 201 });
} finally {
scope.dispose();
}
}Server Actions
Use containers in Server Actions:
TypeScript src/app/actions/user-actions.ts
// src/app/actions/user-actions.ts
'use server';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
import { revalidatePath } from 'next/cache';
export async function createUser(formData: FormData) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const user = await userRepo.create({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
revalidatePath('/users');
return { success: true, user };
} catch (error) {
return { success: false, error: 'Failed to create user' };
} finally {
scope.dispose();
}
}
export async function deleteUser(userId: string) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
await userRepo.delete(userId);
revalidatePath('/users');
return { success: true };
} finally {
scope.dispose();
}
}// src/app/actions/user-actions.ts
'use server';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
import { revalidatePath } from 'next/cache';
export async function createUser(formData: FormData) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const user = await userRepo.create({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
revalidatePath('/users');
return { success: true, user };
} catch (error) {
return { success: false, error: 'Failed to create user' };
} finally {
scope.dispose();
}
}
export async function deleteUser(userId: string) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
await userRepo.delete(userId);
revalidatePath('/users');
return { success: true };
} finally {
scope.dispose();
}
}Server Components
Use the container directly in Server Components:
TypeScript src/app/users/page.tsx
// src/app/users/page.tsx
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
import { UserList } from '@/components/UserList';
export default async function UsersPage() {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const users = await userRepo.findAll();
return (
<div>
<h1>Users</h1>
<UserList users={users} />
</div>
);
} finally {
scope.dispose();
}
}// src/app/users/page.tsx
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
import { UserList } from '@/components/UserList';
export default async function UsersPage() {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const users = await userRepo.findAll();
return (
<div>
<h1>Users</h1>
<UserList users={users} />
</div>
);
} finally {
scope.dispose();
}
}Client-Side Setup
For client components, create React context-based integration:
tsx src/di/client/ScopeContext.tsx
// src/di/client/ScopeContext.tsx
'use client';
import { createContext, useContext } from 'react';
import type { IContainer } from 'ts-ioc-container';
export const ScopeContext = createContext<IContainer | null>(null);
export function useContainer(): IContainer {
const container = useContext(ScopeContext);
if (!container) {
throw new Error('useContainer must be used within a ScopeContext.Provider');
}
return container;
}// src/di/client/ScopeContext.tsx
'use client';
import { createContext, useContext } from 'react';
import type { IContainer } from 'ts-ioc-container';
export const ScopeContext = createContext<IContainer | null>(null);
export function useContainer(): IContainer {
const container = useContext(ScopeContext);
if (!container) {
throw new Error('useContainer must be used within a ScopeContext.Provider');
}
return container;
} TypeScript src/di/client/useInject.ts
// src/di/client/useInject.ts
'use client';
import { useContainer } from './ScopeContext';
import type { constructor, DependencyKey } from 'ts-ioc-container';
export function useInject<T>(key: DependencyKey | constructor<T>): T {
const container = useContainer();
return container.resolve<T>(key);
}// src/di/client/useInject.ts
'use client';
import { useContainer } from './ScopeContext';
import type { constructor, DependencyKey } from 'ts-ioc-container';
export function useInject<T>(key: DependencyKey | constructor<T>): T {
const container = useContainer();
return container.resolve<T>(key);
} tsx src/di/client/Scope.tsx
// src/di/client/Scope.tsx
'use client';
import React, { useEffect, useMemo } from 'react';
import { ScopeContext, useContainer } from './ScopeContext';
import type { Tag } from 'ts-ioc-container';
interface ScopeProps {
tags: Tag[];
children: React.ReactNode;
}
export function Scope({ tags, children }: ScopeProps) {
const parentContainer = useContainer();
const childScope = useMemo(
() => parentContainer.createScope({ tags }),
[parentContainer, ...tags]
);
useEffect(() => {
return () => {
childScope.dispose();
};
}, [childScope]);
return (
<ScopeContext.Provider value={childScope}>
{children}
</ScopeContext.Provider>
);
}// src/di/client/Scope.tsx
'use client';
import React, { useEffect, useMemo } from 'react';
import { ScopeContext, useContainer } from './ScopeContext';
import type { Tag } from 'ts-ioc-container';
interface ScopeProps {
tags: Tag[];
children: React.ReactNode;
}
export function Scope({ tags, children }: ScopeProps) {
const parentContainer = useContainer();
const childScope = useMemo(
() => parentContainer.createScope({ tags }),
[parentContainer, ...tags]
);
useEffect(() => {
return () => {
childScope.dispose();
};
}, [childScope]);
return (
<ScopeContext.Provider value={childScope}>
{children}
</ScopeContext.Provider>
);
}Client Container Configuration
Create a client-specific container:
TypeScript src/di/client/client-container.ts
// src/di/client/client-container.ts
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
} from 'ts-ioc-container';
import { ApiClient } from '@/services/client/ApiClient';
import { AuthStore } from '@/stores/AuthStore';
import { ThemeStore } from '@/stores/ThemeStore';
import { FormStateService } from '@/services/client/FormStateService';
export function createClientContainer() {
return new Container(new MetadataInjector(), { tags: ['application'] })
.addRegistration(
// Application-scoped client services
Registration.fromClass(ApiClient)
.pipe(singleton())
.to('IApiClient'),
Registration.fromClass(AuthStore)
.pipe(singleton())
.to('IAuthStore'),
Registration.fromClass(ThemeStore)
.pipe(singleton())
.to('IThemeStore'),
)
.addRegistration(
// Page-scoped services
Registration.fromClass(FormStateService)
.pipe(scope((s) => s.hasTag('page') || s.hasTag('widget')))
.to('IFormStateService'),
);
}// src/di/client/client-container.ts
import {
Container,
MetadataInjector,
Registration,
singleton,
scope,
} from 'ts-ioc-container';
import { ApiClient } from '@/services/client/ApiClient';
import { AuthStore } from '@/stores/AuthStore';
import { ThemeStore } from '@/stores/ThemeStore';
import { FormStateService } from '@/services/client/FormStateService';
export function createClientContainer() {
return new Container(new MetadataInjector(), { tags: ['application'] })
.addRegistration(
// Application-scoped client services
Registration.fromClass(ApiClient)
.pipe(singleton())
.to('IApiClient'),
Registration.fromClass(AuthStore)
.pipe(singleton())
.to('IAuthStore'),
Registration.fromClass(ThemeStore)
.pipe(singleton())
.to('IThemeStore'),
)
.addRegistration(
// Page-scoped services
Registration.fromClass(FormStateService)
.pipe(scope((s) => s.hasTag('page') || s.hasTag('widget')))
.to('IFormStateService'),
);
}Provider Components
Create a provider component for client-side DI:
tsx src/di/client/ContainerProvider.tsx
// src/di/client/ContainerProvider.tsx
'use client';
import React, { useMemo } from 'react';
import { ScopeContext } from './ScopeContext';
import { createClientContainer } from './client-container';
interface ContainerProviderProps {
children: React.ReactNode;
}
export function ContainerProvider({ children }: ContainerProviderProps) {
const container = useMemo(() => createClientContainer(), []);
return (
<ScopeContext.Provider value={container}>
{children}
</ScopeContext.Provider>
);
}// src/di/client/ContainerProvider.tsx
'use client';
import React, { useMemo } from 'react';
import { ScopeContext } from './ScopeContext';
import { createClientContainer } from './client-container';
interface ContainerProviderProps {
children: React.ReactNode;
}
export function ContainerProvider({ children }: ContainerProviderProps) {
const container = useMemo(() => createClientContainer(), []);
return (
<ScopeContext.Provider value={container}>
{children}
</ScopeContext.Provider>
);
} tsx src/app/layout.tsx
// src/app/layout.tsx
import { ContainerProvider } from '@/di/client/ContainerProvider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ContainerProvider>
{children}
</ContainerProvider>
</body>
</html>
);
}// src/app/layout.tsx
import { ContainerProvider } from '@/di/client/ContainerProvider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ContainerProvider>
{children}
</ContainerProvider>
</body>
</html>
);
}Client Components
Use the container in client components:
tsx src/components/UserForm.tsx
// src/components/UserForm.tsx
'use client';
import { useState } from 'react';
import { Scope, useInject } from '@/di/client';
import type { IFormStateService } from '@/services/client/FormStateService';
import type { IApiClient } from '@/services/client/ApiClient';
import { createUser } from '@/app/actions/user-actions';
function UserFormInner() {
const formState = useInject<IFormStateService>('IFormStateService');
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData();
formData.set('name', name);
formData.set('email', email);
const result = await createUser(formData);
if (result.success) {
setName('');
setEmail('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
/>
<button type="submit">Create User</button>
</form>
);
}
export function UserForm() {
return (
<Scope tags={['widget']}>
<UserFormInner />
</Scope>
);
}// src/components/UserForm.tsx
'use client';
import { useState } from 'react';
import { Scope, useInject } from '@/di/client';
import type { IFormStateService } from '@/services/client/FormStateService';
import type { IApiClient } from '@/services/client/ApiClient';
import { createUser } from '@/app/actions/user-actions';
function UserFormInner() {
const formState = useInject<IFormStateService>('IFormStateService');
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData();
formData.set('name', name);
formData.set('email', email);
const result = await createUser(formData);
if (result.success) {
setName('');
setEmail('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
/>
<button type="submit">Create User</button>
</form>
);
}
export function UserForm() {
return (
<Scope tags={['widget']}>
<UserFormInner />
</Scope>
);
}Pages Router Integration
For Pages Router, use getServerSideProps with request scopes:
tsx src/pages/users/index.tsx
// src/pages/users/index.tsx
import type { GetServerSideProps } from 'next';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
interface UsersPageProps {
users: User[];
}
export default function UsersPage({ users }: UsersPageProps) {
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export const getServerSideProps: GetServerSideProps<UsersPageProps> = async () => {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const users = await userRepo.findAll();
return {
props: { users },
};
} finally {
scope.dispose();
}
};// src/pages/users/index.tsx
import type { GetServerSideProps } from 'next';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
interface UsersPageProps {
users: User[];
}
export default function UsersPage({ users }: UsersPageProps) {
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
export const getServerSideProps: GetServerSideProps<UsersPageProps> = async () => {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
const users = await userRepo.findAll();
return {
props: { users },
};
} finally {
scope.dispose();
}
};API Routes (Pages Router)
TypeScript src/pages/api/users/index.ts
// src/pages/api/users/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
switch (req.method) {
case 'GET': {
const users = await userRepo.findAll();
return res.status(200).json(users);
}
case 'POST': {
const user = await userRepo.create(req.body);
return res.status(201).json(user);
}
default:
return res.status(405).json({ error: 'Method not allowed' });
}
} finally {
scope.dispose();
}
}// src/pages/api/users/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { createRequestScope } from '@/di/server-container';
import type { IUserRepository } from '@/repositories/UserRepository';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const scope = createRequestScope();
try {
const userRepo = scope.resolve<IUserRepository>('IUserRepository');
switch (req.method) {
case 'GET': {
const users = await userRepo.findAll();
return res.status(200).json(users);
}
case 'POST': {
const user = await userRepo.create(req.body);
return res.status(201).json(user);
}
default:
return res.status(405).json({ error: 'Method not allowed' });
}
} finally {
scope.dispose();
}
}Complete Example
Here’s a complete example showing server and client integration:
tsx Complete Next.js Example
// Complete Next.js App Router example
// === src/di/server-container.ts ===
import 'reflect-metadata';
import { Container, MetadataInjector, Registration, singleton, scope } from 'ts-ioc-container';
class UserRepository {
private users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
async findAll() {
return this.users;
}
async create(data: { name: string; email: string }) {
const user = { id: String(this.users.length + 1), ...data };
this.users.push(user);
return user;
}
}
let serverContainer: IContainer | null = null;
export function getServerContainer() {
if (!serverContainer) {
serverContainer = new Container(new MetadataInjector(), { tags: ['application'] })
.addRegistration(
Registration.fromClass(UserRepository)
.pipe(scope((s) => s.hasTag('request')))
.pipe(singleton())
.to('IUserRepository')
);
}
return serverContainer;
}
export function createRequestScope() {
return getServerContainer().createScope({ tags: ['request'] });
}
// === src/app/api/users/route.ts ===
import { NextResponse } from 'next/server';
import { createRequestScope } from '@/di/server-container';
export async function GET() {
const scope = createRequestScope();
try {
const userRepo = scope.resolve('IUserRepository');
const users = await userRepo.findAll();
return NextResponse.json(users);
} finally {
scope.dispose();
}
}
// === src/di/client/ContainerProvider.tsx ===
'use client';
import { createContext, useContext, useMemo, useEffect } from 'react';
import { Container, MetadataInjector, Registration, singleton } from 'ts-ioc-container';
const ScopeContext = createContext(null);
export function useContainer() {
const container = useContext(ScopeContext);
if (!container) throw new Error('No container');
return container;
}
export function useInject(key) {
return useContainer().resolve(key);
}
export function Scope({ tags, children }) {
const parent = useContainer();
const scope = useMemo(() => parent.createScope({ tags }), [parent, ...tags]);
useEffect(() => () => scope.dispose(), [scope]);
return <ScopeContext.Provider value={scope}>{children}</ScopeContext.Provider>;
}
class ApiClient {
async getUsers() {
const res = await fetch('/api/users');
return res.json();
}
}
function createClientContainer() {
return new Container(new MetadataInjector())
.addRegistration(Registration.fromClass(ApiClient).pipe(singleton()).to('IApiClient'));
}
export function ContainerProvider({ children }) {
const container = useMemo(() => createClientContainer(), []);
return <ScopeContext.Provider value={container}>{children}</ScopeContext.Provider>;
}
// === src/app/layout.tsx ===
import { ContainerProvider } from '@/di/client/ContainerProvider';
export default function RootLayout({ children }) {
return (
<html><body>
<ContainerProvider>{children}</ContainerProvider>
</body></html>
);
}
// === src/components/UserList.tsx ===
'use client';
import { useState, useEffect } from 'react';
import { useInject } from '@/di/client/ContainerProvider';
export function UserList() {
const apiClient = useInject('IApiClient');
const [users, setUsers] = useState([]);
useEffect(() => {
apiClient.getUsers().then(setUsers);
}, [apiClient]);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}// Complete Next.js App Router example
// === src/di/server-container.ts ===
import 'reflect-metadata';
import { Container, MetadataInjector, Registration, singleton, scope } from 'ts-ioc-container';
class UserRepository {
private users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
async findAll() {
return this.users;
}
async create(data: { name: string; email: string }) {
const user = { id: String(this.users.length + 1), ...data };
this.users.push(user);
return user;
}
}
let serverContainer: IContainer | null = null;
export function getServerContainer() {
if (!serverContainer) {
serverContainer = new Container(new MetadataInjector(), { tags: ['application'] })
.addRegistration(
Registration.fromClass(UserRepository)
.pipe(scope((s) => s.hasTag('request')))
.pipe(singleton())
.to('IUserRepository')
);
}
return serverContainer;
}
export function createRequestScope() {
return getServerContainer().createScope({ tags: ['request'] });
}
// === src/app/api/users/route.ts ===
import { NextResponse } from 'next/server';
import { createRequestScope } from '@/di/server-container';
export async function GET() {
const scope = createRequestScope();
try {
const userRepo = scope.resolve('IUserRepository');
const users = await userRepo.findAll();
return NextResponse.json(users);
} finally {
scope.dispose();
}
}
// === src/di/client/ContainerProvider.tsx ===
'use client';
import { createContext, useContext, useMemo, useEffect } from 'react';
import { Container, MetadataInjector, Registration, singleton } from 'ts-ioc-container';
const ScopeContext = createContext(null);
export function useContainer() {
const container = useContext(ScopeContext);
if (!container) throw new Error('No container');
return container;
}
export function useInject(key) {
return useContainer().resolve(key);
}
export function Scope({ tags, children }) {
const parent = useContainer();
const scope = useMemo(() => parent.createScope({ tags }), [parent, ...tags]);
useEffect(() => () => scope.dispose(), [scope]);
return <ScopeContext.Provider value={scope}>{children}</ScopeContext.Provider>;
}
class ApiClient {
async getUsers() {
const res = await fetch('/api/users');
return res.json();
}
}
function createClientContainer() {
return new Container(new MetadataInjector())
.addRegistration(Registration.fromClass(ApiClient).pipe(singleton()).to('IApiClient'));
}
export function ContainerProvider({ children }) {
const container = useMemo(() => createClientContainer(), []);
return <ScopeContext.Provider value={container}>{children}</ScopeContext.Provider>;
}
// === src/app/layout.tsx ===
import { ContainerProvider } from '@/di/client/ContainerProvider';
export default function RootLayout({ children }) {
return (
<html><body>
<ContainerProvider>{children}</ContainerProvider>
</body></html>
);
}
// === src/components/UserList.tsx ===
'use client';
import { useState, useEffect } from 'react';
import { useInject } from '@/di/client/ContainerProvider';
export function UserList() {
const apiClient = useInject('IApiClient');
const [users, setUsers] = useState([]);
useEffect(() => {
apiClient.getUsers().then(setUsers);
}, [apiClient]);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}Best Practices
Server vs Client Containers
| Environment | Container | Scope |
|---|---|---|
| Server | Singleton + request scopes | application, request |
| Client | Per-app instance | application, page, widget |
Memory Management
- Server: Always dispose request scopes in
finallyblocks - Client: Use the
Scopecomponent for automatic cleanup - Server Components: Create and dispose scopes within the component
Next.js-Specific Considerations
- Server Container Singleton - The server container persists across requests in development but may be recreated in production
- Client Hydration - Ensure client container is created in
useMemoto prevent hydration mismatches - Server Actions - Create request scopes for each action invocation
- Edge Runtime - Consider using
SimpleInjectorifreflect-metadatacauses issues
API Reference
Server Functions
// Get the singleton server container
function getServerContainer(): IContainer
// Create a request-scoped container
function createRequestScope(): IContainer
Client Components
// Provider component for client-side DI
function ContainerProvider({ children }: { children: React.ReactNode }): JSX.Element
// Hook to get current container
function useContainer(): IContainer
// Hook to resolve dependencies
function useInject<T>(key: DependencyKey | constructor<T>): T
// Scope component for creating child scopes
function Scope({ tags, children }: { tags: Tag[]; children: React.ReactNode }): JSX.Element