npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@svs-tm/react-scope-di

v1.0.0

Published

React bindings for Scope DI with typed injection and async dependency support

Readme

HOC-first React dependency injection for @svs-tm/scope-di.

Use this package when you want React components to consume dependencies from a typed scope-di graph while keeping dependency injection outside the public component props.

The primary API is HOC-style resolution: resolve() and resolveAsync(). Hooks are available for corner cases, but they should be used under a stable scope boundary. Creating scopes, tools, or resolved components during render can cause repeated resolution and render loops.

  • Keep dependencies out of public component props.
  • Preserve typed dependency tuples at the renderer boundary.
  • Resolve sync and async dependencies through stable component wrappers.
  • Use local pending/error fallbacks per component.
  • Create disposable child scopes for component subtrees.
import { configureRootScope, DependencyLifetime } from "@svs-tm/scope-di";
import { createReactDiTools } from "@svs-tm/react-scope-di";

class UsersApi
{
    public async list()
    {
        return ["Ada", "Grace"];
    }
}

const scope = configureRootScope()
    .map("usersApi")
        .asClass(UsersApi, DependencyLifetime.Singleton)
    .map("title")
        .asValue("Users")
    .build();

export const di = createReactDiTools(scope);

Install

pnpm add @svs-tm/scope-di @svs-tm/react-scope-di
npm install @svs-tm/scope-di @svs-tm/react-scope-di
yarn add @svs-tm/scope-di @svs-tm/react-scope-di

@svs-tm/react-scope-di supports React 18 and React 19.

Compatibility

@svs-tm/react-scope-di supports TypeScript 5 and TypeScript 6. Compile-time public API tests run against the latest supported TypeScript 5.x and 6.x compiler versions.

Why HOCs First

resolve() and resolveAsync() create stable component wrappers around dependency resolution. They own the scope boundary behavior, preserve public component props, handle local fallbacks, and avoid the common hook trap where scope construction changes during render.

That makes HOCs the recommended interface for normal application components.

| You want | Use | | --- | --- | | Inject sync dependencies into a component. | resolve() | | Inject awaited async dependencies into a component. | resolveAsync() | | Give a component its own child scope. | createNewScope: true in HOC options. | | Reuse global and local error/pending UI. | HOC options. | | Resolve dependencies inside a custom integration point. | Hooks, under a stable DiScope. |

Quick Start

Create your root DI scope with @svs-tm/scope-di, create React tools from that scope, then use resolve() to build components.

import { configureRootScope, DependencyLifetime } from "@svs-tm/scope-di";
import { createReactDiTools } from "@svs-tm/react-scope-di";

type Logger =
{
    info(message: string): void;
};

class ConsoleLogger implements Logger
{
    public info(message: string)
    {
        console.info(message);
    }
}

const scope = configureRootScope()
    .map("logger")
        .asClass<Logger>(ConsoleLogger, DependencyLifetime.Singleton)
    .map("appName")
        .asValue("Scope DI")
    .build();

const { DiScope, resolve, resolutionOptions } = createReactDiTools(scope);

type HeaderProps =
{
    readonly userId: string;
};

const Header = resolve
(
    ["appName", "logger"],
    resolutionOptions<HeaderProps>(),
    ({ props, dependencies: [appName, logger] }) =>
    {
        logger.info(`Rendering ${appName} for ${props.userId}`);

        return <h1>{appName}</h1>;
    }
);

export function App()
{
    return <Header userId="42" />;
}

Header only accepts HeaderProps. The caller does not pass appName or logger; those are resolved from the DI scope.

| HOC Props | |---|

Create React DI Tools

createReactDiTools(scope) returns a React API bound to your typed DI scope.

const {
    DiScope,
    resolve,
    resolveAsync,
    resolutionOptions,
    asyncResolutionOptions,
    DiAwait,
    useDependencies,
    useDependenciesAsync
} = createReactDiTools(scope);

| Tool | Purpose | | --- | --- | | DiScope | React provider for a DI scope boundary. | | resolve | Recommended HOC-style helper for sync dependency injection. | | resolveAsync | Recommended HOC-style helper for async dependency injection. | | resolutionOptions | Helper for typed sync HOC options. | | asyncResolutionOptions | Helper for typed async HOC options. | | DiAwait | Suspense-aware helper for rendering async hook results. | | useDependencies | Advanced hook for sync dependency resolution. | | useDependenciesAsync | Advanced hook for async dependency resolution. |

Sync Components

Use resolve() when dependencies are available synchronously or when you intentionally want to receive promise-valued dependencies as values.

type UserBadgeProps =
{
    readonly userId: string;
};

const UserBadge = resolve
(
    ["logger", "title"],
    resolutionOptions<UserBadgeProps>(),
    ({ props, dependencies: [logger, title] }) =>
    {
        logger.info(`Rendering badge for ${props.userId}`);

        return <span>{title}</span>;
    }
);

<UserBadge userId="42" />;

The returned component preserves only the public props:

<UserBadge userId="42" />;

Dependencies are inferred as a tuple in the same order as the keys.

Async Components

Use resolveAsync() when dependencies should be awaited before rendering the component.

const scope = configureRootScope()
    .map("profile")
        .asFactoryAsync
        (
            async () => ({ name: "Ada" }),
            DependencyLifetime.Singleton
        )
    .build();

const { DiScope, resolveAsync, asyncResolutionOptions } = createReactDiTools(scope);

type ProfileProps =
{
    readonly userId: string;
};

const Profile = resolveAsync
(
    ["profile"],
    asyncResolutionOptions<ProfileProps>
    (
        {
            pending: { node: <span>Loading profile...</span> },
            error: { component: ({ error }) => <span>Failed to load profile</span> }
        }
    ),
    ({ props, dependencies: [profile] }) =>
    {
        return <h2>{profile.name}</h2>;
    }
);

function ProfilePage()
{
    return <Profile userId="42" />;
}

resolveAsync() schedules async dependency work after React commits the render. This avoids starting async factories for discarded or uncommitted renders.

Generated HOC components receive display names based on the renderer, such as DiResolve(HeaderRenderer) and DiResolveAsync(ProfileRenderer).

| Async HOC | |---|

Error And Pending Fallbacks

You can define global fallbacks when creating the tools.

const di = createReactDiTools
(
    scope,
    {
        error: { component: ({ error }) => <span>Something went wrong</span> }
    }
);

You can override them locally for a specific resolved component.

const Profile = di.resolveAsync
(
    ["profile"],
    di.asyncResolutionOptions
    (
        {
            pending: { node: <span>Loading...</span> },
            error: { component: ({ error }) => <span>Could not load profile</span> }
        }
    ),
    ({ dependencies: [profile] }) => <h2>{profile.name}</h2>
);

Local fallbacks take priority over global fallbacks.

Component Scopes

DiScope creates a stable React scope boundary for resolved components in the subtree.

function App()
{
    return (
        <DiScope>
            <Routes />
        </DiScope>
    );
}

For HOC-style components, pass createNewScope: true when a component should receive its own child DI scope.

type RequestPanelProps =
{
    readonly requestId: string;
};

const RequestPanel = resolve
(
    ["requestState"],
    resolutionOptions<RequestPanelProps>
    (
        {
            createNewScope: true
        }
    ),
    ({ props, dependencies: [requestState] }) =>
    {
        return <section>{requestState.id}</section>;
    }
);

The child scope is disposed when the component unmounts. This is useful for scoped dependencies that should live exactly as long as that component subtree.

Collections

Collection dependencies keep the same typed newest-to-oldest order as the core package.

const scope = configureRootScope()
    .map("toolbarAction")
        .asValue("save")
    .map("toolbarAction")
        .asValue("refresh")
    .map("toolbarAction")
        .asValue("export")
    .build();

const { resolve, resolutionOptions } = createReactDiTools(scope);

const Toolbar = resolve
(
    [["toolbarAction"]],
    resolutionOptions(),
    ({ dependencies: [actions] }) =>
    {
        return (
            <div>
                {actions.map((action) => <button key={action}>{action}</button>)}
            </div>
        );
    }
);

Hooks

Hooks exist for corner cases where an HOC does not fit: custom render integrations, very small local adapters, or code that needs direct access to dependency tuples.

Use hooks only under a stable DiScope boundary. Do not create a DI scope, call createReactDiTools(scope), or create wrapper components inside a component render path. If the scope or resolved component identity changes every render, dependency resolution can repeatedly change component state and cause render loops.

useDependencies

useDependencies() resolves dependencies synchronously and returns a typed tuple in key order.

function UserBadge()
{
    const [logger, title] = useDependencies("logger", "title");

    logger.info("UserBadge rendered");

    return <span>{title}</span>;
}

| Hook resolution | |---|

useDependenciesAsync

useDependenciesAsync() resolves dependencies asynchronously and returns an async result that can be rendered with DiAwait.

function Profile()
{
    const result = useDependenciesAsync("profile");

    return (
        <DiAwait result={result}>
        {
            ([profile]) => <h2>{profile.name}</h2>
        }
        </DiAwait>
    );
}

| Async Hook resolution | |---|

Wrap async hook consumers with React Suspense when you want a loading UI.

import { Suspense } from "react";

function ProfilePage()
{
    return (
        <DiScope>
            <Suspense fallback={<span>Loading profile...</span>}>
                <Profile />
            </Suspense>
        </DiScope>
    );
}

Recommended Project Shape

Keep DI configuration outside components, then export resolved components or the DI tools your app uses.

// di.ts
import { configureRootScope, DependencyLifetime } from "@svs-tm/scope-di";
import { createReactDiTools } from "@svs-tm/react-scope-di";

const scope = configureRootScope()
    .map("usersApi").asClass(UsersApi, DependencyLifetime.Singleton)
    .map("logger").asClass<Logger>(ConsoleLogger, DependencyLifetime.Singleton)
    .build();

export const {
    DiScope,
    resolve,
    resolveAsync,
    resolutionOptions,
    asyncResolutionOptions,
    DiAwait,
    useDependencies,
    useDependenciesAsync
} = createReactDiTools(scope);
// app.tsx
import { DiScope } from "./di";
import { UsersPage } from "./users-page";

export function App()
{
    return (
        <DiScope>
            <UsersPage />
        </DiScope>
    );
}

This keeps component call sites clean while TypeScript carries the registered dependency graph through the resolved components.

License