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-reactive

v2.0.0

Published

A custom React hook to mutate state like an ordinary object with deep reactivity

Readme

React useReactive

react-use-reactive is a custom React hook that lets you mutate state like an ordinary JavaScript object while React manages the re-renders. It supports deep reactivity, even for nested objects.

Installation

To install the react-use-reactive hook, run the following command in your project directory:

npm install react-use-reactive

Usage

First, import the useReactive hook:

import useReactive from 'react-use-reactive';

Next, use the useReactive hook in your functional components:

import React from 'react';
import useReactive from 'react-use-reactive';

const App = () => {
  const state = useReactive({ count: 0, nested: { value: 0 } });

  const incrementCount = () => {
    state.count++;
  };

  const incrementNestedValue = () => {
    state.nested.value++;
  };

  return (
    <div>
      <div>Count: {state.count}</div>
      <button onClick={incrementCount}>Increment Count</button>
      <div>Nested value: {state.nested.value}</div>
      <button onClick={incrementNestedValue}>Increment Nested Value</button>
    </div>
  );
};

export default App;

Usage example with form binding:

...

const state = useReactive({ user: {firstName: '', lastName: ''} });

...

<input
  type="text"
  value={state.user.firstName}
  onChange={(e) => state.user.firstName = e.target.value}
/>
<input
  type="text"
  value={state.user.lastName}
  onChange={(e) => state.user.lastName = e.target.value}
/>

This allows for nested objects to also become reactive, instead of a non-reactive object being assigned to a reactive property.

Semantics

  • Deep reactivity with synchronous reads. The state is an immutable copy-on-write tree behind a mutable-facade Proxy. A commit advances the tree immediately, so consecutive state.count++, +=, and repeated push() calls in one handler all accumulate and reads are never stale. React still re-renders only once per batched tick.
  • Root contract. The initial state must be a plain object or an array (including Object.create(null)). Anything else — undefined, primitives, Date, Map/Set, class instances — throws TypeError("useReactive initial state must be a plain object or array"). Opaque values are valid as nested leaves, not as the root container.
  • Reactive and opaque values. Plain objects and arrays are deep-reactive structural values. Everything else — functions, Date, RegExp, class instances, Map/Set — is opaque: stored by reference, returned as-is, and not proxied. Mutating an opaque object's internals directly (state.date.setFullYear(2030)) is not tracked; replace the value instead (state.date = new Date(...)).
  • Ownership and copy-in. Structural values in the initial value and in every assignment are deep-copied away from the caller: the store owns its tree and caller objects are never mutated. state.b = state.a copies instead of aliasing. Each occurrence of a shared reference is copied into its own independent branch. The model is an owned tree, so cycles are rejected (TypeError("useReactive does not support cyclic plain objects or arrays")) and a rejected single-property assignment never partially attaches.
  • Live read-out references. Assignment into state copies, but reading structural values out returns live reactive references — not snapshots. { ...state }.user === state.user, and slice()/map() results can contain live proxies, so mutating a "copy" can mutate state.
  • Arrays. Array copies preserve exact length, holes, and enumerable own properties (indices, extra string keys, enumerable symbols). A hole and an explicit undefined element differ (in, Object.keys). Array roots (useReactive([1, 2, 3])) are supported.
  • No-op operations (own-property semantics). Only an existing own property is eligible for same-value comparison (Object.is). A missing own property assigned undefined — including push(undefined) and items[5] = undefined — always creates it, and shadowing an inherited key (e.g. state.toString = ...) always creates an own property. Deleting a key with no own property is a no-op. Assigning a structural value always creates a new version, even when it looks equal.
  • Identity (fresh paths). References freshly read from the current tree follow data: the root keeps identity until any data changes, and a nested proxy keeps identity while its own data is unchanged. A previously captured proxy that performs its own write keeps its own identity while its data changes, so key useMemo/React.memo on freshly read paths (state, state.user).
  • Root vs nested held references. An old root proxy keeps following the live root. An old nested proxy is path/logical-node bound: it stays live while its node is current and freezes to the captured node once truly replaced. Removed/reordered array elements (pop, splice, shift, unshift, sort, reverse) are frozen path-bound references; writes through them are inert.
  • Stale writes. Mutations through stale nested references are ignored and can still report success at the Proxy/Reflect level (Reflect.set(stale, ...) === true while state is unchanged).
  • Unsupported shapes fail loudly. Accessor or non-enumerable structural properties (except the standard array length), __proto__ as a structural key, Object.defineProperty/defineProperties, and Object.preventExtensions/freeze/seal all throw a clear TypeError before corrupting state; the tree stays usable afterwards. Enumerable data properties are copied as normal mutable state properties; custom descriptor attributes (writable/configurable flags) are not preserved. delete arr.length throws the language-level invariant TypeError. A rejected single-property assignment never partially attaches; multi-step natives (Object.assign, splice, fill) apply sequentially, so earlier writes may already have applied before a later value throws — the tree stays internally valid.
  • Descriptors are safe. Object.getOwnPropertyDescriptor(state, "user").value returns the same reactive proxy as state.user, never a mutable raw node.
  • Memoization and React Compiler. useMemo(..., [state]), useMemo(..., [state.user]), and React.memo work when keyed on freshly read paths (regression-tested). React Compiler compatibility is not guaranteed: this is an interior-mutability API.
  • Server Components and SSR. The published entry (index.js) starts with the 'use client' directive, so in RSC environments useReactive must be called inside a Client Component. The directive does not disable classic SSR: the hook renders during renderToString and hydrates without recoverable errors. Server and client mounts create independent stores, so render the same initial values on both sides.
  • Mutation during render. Like React state, do not mutate during render; mutate from event handlers and effects.

TypeScript

The package ships with type declarations. index.d.ts is generated from the JSDoc in index.js via npm run build:types — the JSDoc is the source of truth and the two never drift apart.

import useReactive from 'react-use-reactive';

const state = useReactive({ count: 0, user: { name: '' } });
state.count++;
state.user.name = '...';

Known typing limitation: the generic stays T extends object so ordinary interfaces (which lack index signatures) remain assignable. TypeScript therefore cannot exclude opaque roots (Date, Map, class instances) at compile time — they are rejected at runtime instead.

Development

npm ci          # install from lockfile
npm run lint    # oxlint
npm run typecheck  # tsc --noEmit (checks the JSDoc in index.js)
npm test        # vitest
npm run build:types  # regenerate index.d.ts from JSDoc
npm run check:types  # fail if index.d.ts is stale
npm run lint:pack    # publint
npm run check:pack   # verify npm package contents and entry point

Contributing

Contributions are welcome! If you find a bug or have a feature request, please open an issue on GitHub. If you'd like to contribute code, please fork the repository and make a pull request.