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.

Next.js has unique requirements due to its hybrid rendering model:

Scope Hierarchy in Next.js

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'] });
}

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();
}
}

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();
}
}

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();
}
}

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>
);
}

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'),
  );
}

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>
);
}

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>
);
}

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();
}
};
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();
}
}

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>
);
}

Server vs Client Containers

EnvironmentContainerScope
ServerSingleton + request scopesapplication, request
ClientPer-app instanceapplication, page, widget

Memory Management

Next.js-Specific Considerations

  1. Server Container Singleton - The server container persists across requests in development but may be recreated in production
  2. Client Hydration - Ensure client container is created in useMemo to prevent hydration mismatches
  3. Server Actions - Create request scopes for each action invocation
  4. Edge Runtime - Consider using SimpleInjector if reflect-metadata causes issues

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