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

use-controllable

v1.1.3

Published

A React hook for managing controllable and uncontrollable component states

Downloads

23

Readme

use-controllable

A React hook for managing both controlled and uncontrolled component states.

If the parent provides the value and the onChange callback, the hook will use those to handle the component state, if not, the component creates an internal state to still working

Table of Contents

Why Support Both Modes?

Building components that support both controlled and uncontrolled modes in a single implementation is crucial for creating flexible, reusable components. Here's why this pattern matters:

⚡ Performance: Avoid Unnecessary Re-renders

When a component is controlled (receives value prop), useControllable directly uses the controlled value without creating internal state. This means:

  • No state synchronization: The hook doesn't maintain a separate internal state when controlled
  • Fewer re-renders: Changes to the controlled value don't trigger both internal state updates AND prop updates
  • Single source of truth: The value flows directly from props to the component

Without this pattern:

// ❌ Anti-pattern: Always using internal state
function BadComponent({ value, onChange }) {
  const [internalValue, setInternalValue] = useState(value);

  // Need to sync internal state with prop changes
  useEffect(() => {
    setInternalValue(value); // Extra re-render!
  }, [value]);

  // ... causes multiple onChange calls and state conflicts
}

With useControllable:

// ✅ Optimal: Direct value usage when controlled
function GoodComponent({ value, onChange }) {
  const [currentValue, setValue] = useControllable({ value, onChange });
  // When controlled: currentValue === value (no internal state)
  // When uncontrolled: currentValue is managed internally
}

🔄 Avoid Multiple onChange Calls

Improper state synchronization can lead to onChange being called multiple times for a single user action:

  • User changes input → triggers setValue → updates internal state → syncs with prop → triggers onChange again
  • This creates confusing behavior and potential infinite loops

useControllable ensures onChange is called exactly once per value change.

🧹 Code Simplification

Without this hook, you'd need to:

  1. Check if the component is controlled or uncontrolled
  2. Manage conditional state creation
  3. Handle state synchronization with useEffect
  4. Prevent the component from switching between modes
  5. Handle edge cases and race conditions

useControllable handles all of this in one line, making your component code cleaner and less error-prone.

🎯 Single Component, Multiple Use Cases

Users of your component can choose what works best for their needs:

  • Controlled: Full control over state, useful for forms, validation, external state management
  • Uncontrolled: Simpler usage for basic cases, less boilerplate

Same component, zero code duplication.

Installation

npm install use-controllable
# or
pnpm add use-controllable
# or
yarn add use-controllable

Features

  • 🎯 Flexible: Works in both controlled and uncontrolled modes
  • 🪶 Lightweight: Minimal bundle size (~0.3KB gzipped)
  • 📘 TypeScript: Full TypeScript support with type inference
  • ⚛️ React 18 & 19: Compatible with React 18 and 19
  • 🧪 Well-tested: Comprehensive test coverage

Usage

Uncontrolled Mode

When value is not provided, the component manages its own state internally.

You can provide defaultValue to set the initial value in the uncontrolled state

import { useControllable } from "use-controllable";

function MyComponent() {
  const [value, setValue] = useControllable({
    defaultValue: "hello",
    onChange: (v) => console.log("Changed to:", v),
  });

  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

Controlled Mode

When value is provided, the component is controlled externally:

import { useControllable } from "use-controllable";

function MyComponent({ value, onChange }) {
  const [controlledValue, setControlledValue] = useControllable({
    value,
    onChange,
  });

  return (
    <input
      value={controlledValue}
      onChange={(e) => setControlledValue(e.target.value)}
    />
  );
}

API

useControllable<T>(params)

Parameters

  • params.value?: T - The controlled value (optional). When provided, the hook operates in controlled mode.
  • params.defaultValue?: T - The default value for uncontrolled mode. Only used when value is not provided.
  • params.onChange?: (value: T) => void - Callback function called when the value changes.

Returns

Returns a tuple [value, setValue] similar to useState:

  • value: T | undefined - The current value
  • setValue: (value: T) => void - Function to update the value

TypeScript

The hook is fully typed and will infer types from your usage:

// Type is inferred as string
const [value, setValue] = useControllable({
  defaultValue: "hello",
});

// Type is explicitly set
const [value, setValue] = useControllable<number>({
  defaultValue: 42,
});

Using UseControllableProps Type

To type the parent component props in a correct way you can use the the UseControllableProps type helper to ensure your component props correctly support both controlled and uncontrolled modes bu not both at the same time:

import { useControllable, type UseControllableProps } from 'use-controllable'

// Example 1: Standard value/onChange pattern
type MyComponentProps = UseControllableProps<string> & {
  placeholder?: string
}

function MyComponent(props: MyComponentProps) {
  const [value, setValue] = useControllable({
    value: props.value,
    defaultValue: props.defaultValue,
    onChange: props.onChange,
  })

  return <input value={value} onChange={(e) => setValue(e.target.value)} />
}

// Usage - Controlled
<MyComponent value={externalValue} onChange={setExternalValue} />

// Usage - Uncontrolled
<MyComponent defaultValue="initial" />

Custom Property Names

You can also customize the property name from value , onChange and default to something else:

// Example 2: Custom property name (e.g., 'checked' for a checkbox)
type ToggleProps = UseControllableProps<boolean, 'checked'> & {
  label?: string
}

function Toggle(props: ToggleProps) {
  const [checked, setChecked] = useControllable({
    value: props.checked,
    defaultValue: props.defaultChecked,
    onChange: props.onChangeChecked,
  })

  return (
    <button onClick={() => setChecked(!checked)}>
      {props.label} {checked ? '✓' : '○'}
    </button>
  )
}

// Usage - Controlled
<Toggle checked={isChecked} onChangeChecked={setIsChecked} />

// Usage - Uncontrolled
<Toggle defaultChecked={false} />

What UseControllableProps Does

The type ensures:

  • Controlled mode: When value is provided, onChange is available but defaultValue is not
  • Uncontrolled mode: When value is not provided, defaultValue is available but onChange is not
  • Type safety prevents mixing controlled and uncontrolled props incorrectly

Running Examples

The repository includes interactive examples demonstrating various use cases:

# Install dependencies
pnpm install

# Start the development server
pnpm dev

Then open your browser to the URL shown (typically http://localhost:5173).

The examples include:

  • Uncontrolled Input: Component managing its own state
  • Controlled Input: Component controlled by parent state
  • Toggle Component: Custom component with boolean state
  • Flexible Component: Demonstrates switching between controlled/uncontrolled modes

Performance

The hook is designed for optimal performance. Benchmarks comparing useControllable against the traditional approach (internal state + useEffect synchronization) show significant improvements:

  • Fewer re-renders: No extra re-renders from state synchronization in controlled mode
  • No useEffect overhead: Direct value usage eliminates sync logic
  • Memory efficient: Single state source instead of maintaining both internal state and watching props

Run the benchmarks yourself:

pnpm bench

The benchmark tests compare:

  • Component without useControllable (uses internal state + useEffect to sync with controlled value)
  • Component with useControllable (optimal implementation)

Scenarios tested:

  • Initial render performance
  • Re-render performance with value changes
  • State update performance
  • Memory efficiency with multiple instances

Benchmark Results

Real-world performance comparison on a typical development machine:

| Scenario | Without Hook | With useControllable | Performance Gain | | ---------------------------------------------- | ------------- | -------------------- | --------------------- | | Controlled mode - Initial render | 1,167 ops/sec | 1,631 ops/sec | 1.40x faster ⚡ | | Controlled mode - Re-renders (100 updates) | 40 ops/sec | 87 ops/sec | 2.14x faster ⚡⚡ | | Uncontrolled mode - Initial render | 2,007 ops/sec | 1,981 ops/sec | ~1.01x (equivalent) | | Multiple instances (100 components) | 62 ops/sec | 94 ops/sec | 1.51x faster ⚡ |

Key Takeaways:

  • 2.14x faster re-renders in controlled mode - the most common use case
  • 1.40x faster initial renders when controlled
  • 1.51x faster when rendering multiple component instances
  • 🟰 Equivalent performance in uncontrolled mode (no overhead added)

The performance gains are most significant in controlled components with frequent updates, exactly where traditional approaches with useEffect synchronization struggle the most.

Development

# Install dependencies
pnpm install

# Run examples locally
pnpm dev

# Run tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run tests in ui mode
pnpm test:ui

# Run benchmarks
pnpm bench

# Build
pnpm build

# Lint
pnpm lint

# Format
pnpm format

# Type check
pnpm typecheck

License

MIT