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-use-control

v1.3.2

Published

Make React component state controllable — seamless controlled/uncontrolled support via a lightweight control object.

Readme

CI install size

react-use-control

Make React component state controllable — a tiny (~80 LOC) utility for building components that seamlessly support both controlled and uncontrolled modes.

Motivation

In React, component authors often need to support two usage patterns:

  • Uncontrolled: the component manages its own state internally (defaultValue).
  • Controlled: a parent component owns the state and passes it down (value + onChange).

Supporting both typically requires boilerplate: checking whether a prop is undefined, syncing internal state with external props via useEffect, and carefully handling edge cases. Libraries like @radix-ui/react-use-controllable-state solve this with a prop / defaultProp / onChange pattern.

react-use-control takes a different approach. Instead of passing values and callbacks separately, it introduces a control object — an opaque token that carries state authority through the component tree. Whoever creates the state first owns it; everyone else defers. This enables:

  • Zero-boilerplate controlled/uncontrolled support
  • State sharing across sibling components (not just parent → child)
  • Middleware-style state transforms via useThru
  • Re-render optimization — control refs are stable when values don't change, so React.memo works out of the box

Install

npm install react-use-control

Quick Start

Basic: Uncontrolled Component

When no control is passed, the component manages its own state:

import {useControl} from 'react-use-control';

function Counter() {
  const [count, setCount] = useControl(0);
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

// Usage: <Counter />  — works independently

Controlled by Parent

Pass a control object to let a parent own the state:

function Parent() {
  const [count, setCount, countCtrl] = useControl(0);

  return (
    <div>
      <Counter count={countCtrl} />
      <button onClick={() => setCount(0)}>Reset</button>
      <p>Parent sees: {count}</p>
    </div>
  );
}

function Counter({count}) {
  const [num, setNum] = useControl(count, 0);
  return <button onClick={() => setNum((n) => n + 1)}>{num}</button>;
}

Sharing State Across Siblings

The same control can be passed to multiple children — they all share the same state:

function App() {
  const [, setCount, countCtl] = useControl(0);

  return (
    <div>
      <Counter count={countCtl} />
      <Counter count={countCtl} />
      <button onClick={() => setCount(0)}>Reset Both</button>
    </div>
  );
}

How It Works

graph TD
    P["Parent: useControl(0)<br/>→ creates state, returns [value, setValue, control]"]
    P -- "control (as prop)" --> A
    P -- "control (as prop)" --> B
    A["Child A: useControl(control, 1)<br/>State already exists → reuses it<br/>No new state created"]
    B["Child B: useControl(control, 1)<br/>State already exists → reuses it<br/>No new state created"]

When a child calls useControl(control, initial), it checks whether state has already been created upstream. If so, the child reuses it directly; otherwise it creates local state. This means:

  • No context providers needed — state flows through props
  • No useEffect synchronization — parent and child share the same state, not two copies kept in sync
  • initial is ignored when controlled — just like React's useState

For a deeper dive into the problem and the design rationale, see Who Owns the State? Rethinking Controlled/Uncontrolled Components in React.

API

useControl(controlOrInitial?, initial?)

function useControl<S>(
  control: Control<S> | null | undefined,
  initial: S | (() => S)
): [S, Dispatch<SetStateAction<S>>, Control<S>];

function useControl<S>(
  initial: S | (() => S)
): [S, Dispatch<SetStateAction<S>>, Control<S>];
  • controlOrInitial — a control object from a parent, an initial state value, or null/undefined for uncontrolled mode. When a non-control value is passed, it is used as the initial state directly.
  • initial — initial state value as the second argument (ignored when controlled). When the first argument is not a control, the first argument takes precedence.
  • Returns [value, setValue, control] — same shape as useState, plus the control object for passing to children.

useThru(control, interceptor)

function useThru<S>(
  control: Control<S> | null | undefined,
  interceptor: (state: [S, SetState<S>]) => [S, SetState<S>]
): Control<S>;

Insert a middleware that transforms state or setter before passing to children:

import {useThru, mapSetter} from 'react-use-control';

function DoubleOnSet({count}) {
  const control = useThru(
    count,
    mapSetter((v) => v * 2)
  );
  return <Counter count={control} />;
}

mapState(fn)

Transform the state value read by children:

mapState((count) => count * 100); // children see count × 100

mapSetter(fn)

Transform the value before it reaches setState:

mapSetter((v) => Math.max(0, v)); // clamp to non-negative

watch(onChange)

Side-effect on state changes (logging, analytics, etc.):

watch((v) => console.log('new value:', v));

isControl(value)

Type guard to check if a value is a control object:

isControl(someValue); // true | false

Comparison with Other Approaches

| Feature | react-use-control | @radix-ui/react-use-controllable-state | Manual (useState + useEffect) | | ------------------------ | ------------------------------------ | -------------------------------------- | --------------------------------- | | Controlled/Uncontrolled | ✅ Automatic via control object | ✅ Via prop/defaultProp/onChange | ⚠️ Manual boilerplate | | State sharing (siblings) | ✅ Same control to multiple children | ❌ Not supported | ❌ Lift state + pass individually | | Middleware transforms | ✅ useThru + composable transforms | ❌ Not supported | ❌ Manual wrappers | | Re-render optimization | ✅ Stable control refs + React.memo | ✅ Standard React patterns | ⚠️ Depends on implementation | | Bundle size | ~80 LOC, zero deps | ~150 LOC, 2 internal deps | N/A | | Learning curve | Medium (control object concept) | Low (familiar prop pattern) | Low | | Ecosystem adoption | Niche | Widely used (Radix, shadcn/ui) | Universal |

When to choose react-use-control:

  • Any React component that exposes internal state to its parent — forms, toggles, dialogs, tabs, filters, etc.
  • Sibling components that need to share state without lifting it manually
  • State flow that benefits from middleware-style transforms (clamping, logging, mapping)
  • You prefer a single prop (control) over the value/defaultValue/onChange triple

When to choose radix or manual approach:

  • You need maximum ecosystem familiarity
  • You're already using Radix UI primitives and want to stay consistent

Workflow

# develop with watch mode
npm start

# run tests
npm test

# build
npm run build

# storybook
npm run storybook

# commit changes
npm run commit

License

MIT