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

react-controller-context

v2.0.1

Published

Generate typed React Context + Provider pairs from controller hooks

Downloads

537

Readme

React Controller Context

Generate a typed React Context + Provider pair from any controller hook with a single factory call: createControllerContext. Zero boilerplate, full type inference, runtime safety, zero dependencies.

Installation

npm install react-controller-context

Peer dependencies: react 18 or 19.

Quick Start

import { createControllerContext } from 'react-controller-context';

// 1. Write a controller: a hook that takes one props object
const useCounter = ({ initialValue }: { initialValue?: number } = {}) => {
    const [count, setCount] = React.useState(initialValue ?? 0);
    return { count, setCount };
};

// 2. Create the controller context
const Counter = createControllerContext(useCounter, 'Counter');

// 3. Provide it. The Provider's props mirror your controller's props
const App = () => (
    <Counter.Provider initialValue={4}>
        <Display />
    </Counter.Provider>
);

// 4. Consume it anywhere underneath
const Display = () => {
    const { count, setCount } = Counter.use();
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
};

API

createControllerContext(useController, name?)

| Param | Type | Description | |---|---|---| | useController | (props: P) => R | A controller hook. It must take a single props object (or nothing); its return value becomes the context value. | | name | string (optional) | Label for error messages and React DevTools. Falls back to the controller function's name, then a generic label. Pass it explicitly if the message must survive minification. |

Returns a controller context:

| Key | Type | Description | |---|---|---| | Provider | React.FC<PropsWithChildren<P>> | Forwards all non-children props to the controller and supplies its return value to descendants. Prop types are inferred from the controller's parameter. | | use | () => R | Returns the controller value from the nearest Provider. Throws a descriptive error when called without one. | | context | React.Context<R> | The raw context, an escape hatch for React 19's use(context) or injecting a mock value in tests. |

Runtime safety

Calling use() without a Provider above it throws immediately:

Error: Counter cannot be used outside its Provider.

Detection uses a unique sentinel, so controllers that legitimately return falsy values (0, '', false, null) work fine.

Note: despite the name, use() is a regular hook. Call it at the top level of your component, not inside conditionals. (It wraps useContext for React 18 compatibility, so React 19's conditional-use superpower does not apply.)

Recipes

Controller with no props

A controller can take no parameter at all. The Provider then accepts only children:

const useTheme = () => {
    const [dark, setDark] = React.useState(false);
    return { dark, toggle: () => setDark(d => !d) };
};

const Theme = createControllerContext(useTheme, 'Theme');

<Theme.Provider>
    <App />
</Theme.Provider>

Required props

Required fields in the controller's props become required Provider props, and TypeScript enforces them at the call site:

const useAuth = ({ userId }: { userId: string }) => { /* ... */ };
const Auth = createControllerContext(useAuth, 'Auth');

<Auth.Provider userId="u-42">...</Auth.Provider>  // OK
<Auth.Provider>...</Auth.Provider>                // compile error: userId missing
<Auth.Provider userid="u-42">...</Auth.Provider>  // compile error: typo caught

Composing multiple controller contexts

Each controller context is independent, and a later Provider's controller can consume an earlier one. Rather than hand-nesting Providers, compose them with react-compose-provider:

import { composeProvider } from 'react-compose-provider';

const useCart = () => {
    const { userId } = Auth.use();   // controllers are hooks; they can use() other contexts
    return useCartForUser(userId);
};
const Cart = createControllerContext(useCart, 'Cart');

// First provider is outermost; providers needing props get a small wrapper
const AppProviders = composeProvider(
    ({ children }) => <Auth.Provider userId="u-42">{children}</Auth.Provider>,
    Cart.Provider,
);

const App = () => (
    <AppProviders>
        <Checkout />
    </AppProviders>
);

Mounting the same Provider twice follows normal React context rules: use() reads from the nearest one above.

Testing components without running the real controller

The raw context lets tests inject a value directly, skipping the controller's state, effects, and network calls:

render(
    <Counter.context.Provider value={{ count: 99, setCount: jest.fn() }}>
        <Display />
    </Counter.context.Provider>,
);

React 19 escape hatch

On React 19 you can read the context with the native use(), including conditionally:

const value = use(Counter.context);  // warning: bypasses the missing-Provider guard

Prefer Counter.use() everywhere else; it is the one that throws a helpful error.

Edge cases & gotchas

  • children is reserved. The Provider keeps children for the React tree and forwards everything else, so a controller prop named children will never arrive. Name it something else (items, nodes, etc.).
  • Re-renders follow normal context rules. Every use() consumer re-renders when the controller's return value changes identity. If your controller returns a fresh object each render, wrap it: return React.useMemo(() => ({ count, setCount }), [count]);
  • One props object, not positional arguments. A controller like useThing(initialValue?: string) can't be wired to JSX props because there is no runtime mapping from prop names to parameter positions. Take { initialValue }: { initialValue?: string } instead.
  • Minified error labels. When name is omitted, the error/DevTools label falls back to the controller's runtime function name, which production minifiers may mangle. Pass an explicit name if you care about prod error messages.

Migrating from 1.x

The 1.x export createContextForController was removed in 2.0.0.

// 1.x
const ctx = createContextForController(useMyHook);
<ctx.Provider options={{ initialValue: '1234' }}>
const value = ctx.useController();

// 2.x
const MyThing = createControllerContext(useMyHook, 'MyThing');
<MyThing.Provider initialValue="1234">
const value = MyThing.use();

Key changes:

  • Controllers must take a single props object (the 1.x options argument was untyped; provider props are now spread and fully typed by inference).
  • The consumer hook is named use and throws outside a Provider instead of silently returning an empty object.
  • An optional name powers error messages and DevTools.

License

MIT