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-vue-computed

v1.0.3

Published

Vue's computed mental model brought to React via Proxy and useSyncExternalStore

Readme

react-vue-computed

Vue 3's computed mental model brought into React: auto-tracked dependencies, lazy memoization, and re-renders that fire only when the derived value actually changes.

Built on JavaScript Proxy and React's useSyncExternalStore. No dependency arrays.

Features

  • No dependency arrays. Dependencies are auto-tracked at runtime by reading reactive sources inside the getter.
  • Lazy memoization. The getter only re-runs when one of its tracked dependencies has actually changed.
  • Reference-equal re-render skipping. A derived value that comes out reference-equal to the previous one (Object.is) does not trigger a React re-render, even if upstream state changed.
  • Writable computed. Pass { get, set } to get a [value, setValue] tuple for two-way derived state.
  • TypeScript-first. Full overload-based typing, no casts needed at call sites.

Installation

npm install react-vue-computed

Requires React 18 or newer (for useSyncExternalStore).

Why this exists

React Compiler closes much of the manual-memoization gap (useMemo / useCallback deps arrays). What it does not change is the underlying model: components are pull-based, memoization happens at the component level, and a state write triggers re-renders for any component reading that state through the React tree.

This library gives you push-based reactivity with property-level dependency tracking. A write to state.foo only invalidates derivations that actually read foo — not anything that reads state more broadly. Useful for cross-cutting application state read by many components.

If you want all of Vue's reactivity API (watch, watchEffect, effectScope, etc.) you want a fuller-featured library; this one is intentionally scoped to ref, reactive, and computed-via-hook.

Usage

Read-only computed

import { ref, useComputed } from 'react-vue-computed';

const firstName = ref('John');
const lastName = ref('Doe');

export function Greeting() {
  const fullName = useComputed(() => `${firstName.value} ${lastName.value}`);
  return <h1>Hello, {fullName}</h1>;
}

No deps array. The hook subscribes to firstName and lastName automatically because they were read inside the getter. Mutating either will trigger a re-render of Greeting if and only if the joined string actually changed.

Reactive objects

For object-shaped state, use reactive instead of multiple refs:

import { reactive, useComputed } from 'react-vue-computed';

const cart = reactive({
  items: [] as Array<{ price: number; qty: number }>,
  taxRate: 0.15,
});

export function CartTotal() {
  const total = useComputed(() => {
    const subtotal = cart.items.reduce((s, i) => s + i.price * i.qty, 0);
    return subtotal * (1 + cart.taxRate);
  });
  return <p>Total: ${total.toFixed(2)}</p>;
}

reactive() returns a deep Proxy. Property reads are tracked, writes (including delete) trigger anything subscribed to that specific property.

Writable computed

Pass { get, set } to get a [value, setValue] tuple:

import { ref, useComputed } from 'react-vue-computed';

const basePrice = ref(100);
const TAX_RATE = 1.15;

export function PriceEditor() {
  const [withTax, setWithTax] = useComputed({
    get: () => basePrice.value * TAX_RATE,
    set: (v: number) => {
      basePrice.value = v / TAX_RATE;
    },
  });

  return (
    <input
      type="number"
      value={withTax}
      onChange={(e) => setWithTax(Number(e.target.value))}
    />
  );
}

When the setter writes to basePrice, anything else deriving from basePrice updates too.

Helpers

import { ref, isRef, unref, type Ref } from 'react-vue-computed';

const r = ref(7);
isRef(r);        // true
isRef(7);        // false
unref(r);        // 7
unref(7);        // 7

Constraints (read this part)

This library only tracks values read through ref() or reactive(). There is no magic that intercepts plain variables.

Do not capture React props or local state in the getter and expect them to invalidate the cache. They won't:

function Broken({ multiplier }: { multiplier: number }) {
  // ❌ multiplier change will NOT invalidate the cache,
  //    because multiplier is not a reactive source.
  const result = useComputed(() => count.value * multiplier);
}

The getter closure stays "live" — it always sees the latest multiplier when it runs — but the cache is only invalidated when a tracked reactive source changes. If count doesn't change, the cache won't recompute, and the prop change won't be reflected.

This is the same constraint Vue users follow. Either lift the dependency into the reactive system, or don't use useComputed for that derivation:

function Working({ multiplier }: { multiplier: number }) {
  // Just compute it directly; React Compiler / useMemo handle the rest.
  const result = count.value /* if reading reactive in render works for you */ * multiplier;
}

Returning fresh objects every recompute will trigger re-renders. useSyncExternalStore compares snapshots with Object.is. A getter that returns { ...state } produces a new reference every run. If you want stable identity, return the underlying reactive object directly, or wrap with your own equality check.

How it works

Three pieces:

  1. ref / reactive wrap state. Reads call an internal track() that adds the currently running effect to a per-source Set<Effect>. Writes call trigger() that runs every effect in the set.
  2. ComputedImpl is itself an effect. When its getter runs, the global activeEffect is set to its own effect, so any reactive read inside the getter subscribes the computed. Re-runs are lazy and only happen on the next .value read after a dep change. Cleanup removes stale subscriptions before each re-run, so conditional reads work correctly.
  3. useComputed wires the computed into React via useSyncExternalStore. The store's subscribe adds React's notify callback; getSnapshot returns the lazily-computed value. React's built-in Object.is comparison on snapshots is what gives you the "skip re-render if value unchanged" behavior.

License

MIT