Skip to content

sadkebab/superctx

Repository files navigation

SuperCtx

An ergonomic superset of React's Context API for butter smooth dependency injection 🧈

Warning

This library is meant to be used with the React Compiler. Relying heavily on the Context API without the React Compiler can cause serious performance sinks in your application.

Overview

superctx extends React's Context API with:

  • βœ… Better TypeScript inference than native Context API
  • βœ… Automatic error handling for missing providers
  • βœ… Enhanced base context providers with addBase
  • βœ… Easy access to multiple contexts
  • βœ… Lazy initialization of default values
  • βœ… Zero dependencies (only uses React)

Especially recommended for AI coding agents:

  • βœ… Less boilerplate means lower token consumption
  • βœ… More compact imports keep prompts and diffs shorter
  • βœ… Lower token usage leads to better iteration quality

Requirements

  • React 19 or above

Installation

pnpm add superctx

Usage

Basic Context Creation

import { createSuperContext } from "superctx";

// Context that requires a provider
const UserContext = createSuperContext<{ name: string; email: string }>({
  notProvidedMessage: "User context not provided",
});

// Context with default value
const ThemeContext = createSuperContext<"light" | "dark">({
  initialValue: "light",
});

// Context with lazy initialization
const ConfigContext = createSuperContext<AppConfig>({
  getInitialValue: () => loadConfigFromStorage(),
});

Using Contexts

function UserProfile() {
  const user = UserContext.useProvided();

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Consumer pattern (alternative to hook)
function UserDisplay() {
  return <UserContext.Consumer>{(user) => <div>{user.name}</div>}</UserContext.Consumer>;
}

Base Components with addBase

Create root components that automatically provide context values:

import { createSuperContext, addBase } from "superctx";

const Environment = addBase(
  createSuperContext<NebulyEnvironment>({
    notProvidedMessage: "Environment not provided",
  }),
  (Provider) => {
    return ({ children }) => {
      const env = getEnvironmentValues();
      return <Provider value={env}>{children}</Provider>;
    };
  },
);

// Usage - just use the Base component
function App() {
  return (
    <Environment.Base>
      <YourApp />
    </Environment.Base>
  );
}

// Access the context anywhere
function Component() {
  const env = Environment.useProvided();
  return <div>{env.apiUrl}</div>;
}

Working with Multiple Contexts

Use useProviders to access multiple contexts at once:

import { useProviders } from "superctx";

function MyComponent() {
  const [user, theme, environment] = useProviders([UserContext, ThemeContext, Environment]);

  return (
    <div className={theme}>
      <h1>{user.name}</h1>
      <p>API: {environment.apiUrl}</p>
    </div>
  );
}

Consumer Component

Use the Consumer component for cleaner conditional rendering with multiple contexts:

import { Consumer } from "superctx";

function DataDisplay() {
  return (
    <Consumer providers={[UserContext, ProjectContext]}>
      {([user, project]) => (
        <div>
          <h1>{user.name}</h1>
          <h2>{project.name}</h2>
        </div>
      )}
    </Consumer>
  );
}

Real-World Examples

Environment Configuration

import { createSuperContext, addBase } from "superctx";

type NebulyEnvironment = {
  apiUrl: string;
  environment: "development" | "production";
  flags?: Record<string, boolean>;
};

export const Environment = addBase(
  createSuperContext<NebulyEnvironment>({
    notProvidedMessage: "Environment not provided",
  }),
  (Provider) => {
    return ({ children }) => {
      const env = getEnvironmentValues();
      return <Provider value={env}>{children}</Provider>;
    };
  },
);

// Usage
function App() {
  return (
    <Environment.Base>
      <Router />
    </Environment.Base>
  );
}

function ApiClient() {
  const { apiUrl } = Environment.useProvided();
  // Use apiUrl to configure API client
}

Theme Context

import { createSuperContext, addBase } from "superctx";

export const Theme = addBase(
  createSuperContext<"light" | "dark" | "system">({
    initialValue: "system",
  }),
  (Provider) => {
    return ({ children, defaultTheme = "system" }) => {
      const [theme, setTheme] = useState(defaultTheme);
      return <Provider value={theme}>{children}</Provider>;
    };
  },
);

// Usage
function App() {
  return (
    <Theme.Base defaultTheme="light">
      <YourApp />
    </Theme.Base>
  );
}

Combining Multiple Contexts

import { useProviders, Consumer } from "superctx";

// Access multiple contexts
function Dashboard() {
  const [user, project, environment] = useProviders([UserContext, ProjectContext, Environment]);

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>Project: {project.name}</p>
      <p>Environment: {environment.environment}</p>
    </div>
  );
}

// Or use Consumer component for conditional rendering
function ConditionalContent() {
  return (
    <Consumer providers={[UserContext, FeatureFlags]}>
      {([user, flags]) => {
        if (flags.isAdmin && user.role === "admin") {
          return <AdminPanel />;
        }
        return <RegularContent />;
      }}
    </Consumer>
  );
}

Formatters with Locale

import { createSuperContext, addBase } from "superctx";
import { DateFormatter, NumberFormatter, TimeFormatter } from "formatters";

export const Formatters = addBase(
  createSuperContext<{
    numberFmt: NumberFormatter;
    timeFmt: TimeFormatter;
    dateFmt: DateFormatter;
  }>({
    notProvidedMessage: "Formatters are not provided",
  }),
  (Provider) => {
    return ({ children }) => {
      const { language } = Localization.useProvided();
      return (
        <Provider
          value={{
            numberFmt: new NumberFormatter({ locale: language }),
            timeFmt: new TimeFormatter({ locale: language }),
            dateFmt: new DateFormatter({ locale: language }),
          }}
        >
          {children}
        </Provider>
      );
    };
  },
);

// Usage
function PriceDisplay({ amount }: { amount: number }) {
  const { numberFmt } = Formatters.useProvided();
  return <span>{numberFmt.compactFloat(amount)}</span>;
}

API Reference

createSuperContext<T>(options)

Creates a super context with enhanced features.

Options:

  • initialValue: T: Default value (context is always provided)
  • getInitialValue: () => T: Lazy initialization function
  • notProvidedMessage: string: Error message if provider is missing

Returns:

  • Provider: React context provider component
  • Consumer: React context consumer component
  • useProvided(): Hook to access context value

addBase<T, U>(context, baseFactory)

Creates a base component for a context.

Parameters:

  • context: A super context
  • baseFactory: Function that takes Provider and returns a base component

Returns: Context with added Base component

useProviders<T>(deps)

Hook to access multiple contexts at once.

Parameters:

  • deps: Array of super contexts

Returns: Tuple of context values (typed)

Consumer<T>(props)

Component for conditional rendering with multiple contexts.

Props:

  • providers: Array of super contexts
  • children: Render function receiving context values

MissingProviderError

Error class thrown when a required context is not provided.

Error Handling

When a required context is missing, superctx throws a MissingProviderError as soon as you try to read it.

This fail-fast behavior is intentional: it surfaces provider mistakes immediately, instead of silently propagating null values and causing harder-to-debug errors later.

Vanilla

import { createContext, useContext } from "react";

export const MyContext = createContext<{
  foo: "bar";
}>(null!);

export function useMyContext() {
  const value = useContext(MyContext);
  if (!value) {
    throw new Error("useMyContext requires a MyContext provider");
  }
  return value;
}

export function SomeComponent() {
  const { foo } = useMyContext();
  return <p>{foo}</p>;
}

With superctx

import { createSuperContext } from "superctx";

export const MyContext = createSuperContext<{
  foo: "bar";
}>({
  notProvidedMessage: "MyContext provider is missing",
});

export function SomeComponent() {
  const { foo } = MyContext.useProvided();
  return <p>{foo}</p>;
}

About

An ergonomic superset of React's Context API for butter smooth dependency injection 🧈

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors