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

@matthesketh/ink-stable-state

v0.1.0

Published

useStableState for Ink: a useState replacement that skips re-renders when the new value is structurally equal to the old one. Kills polling-induced flicker.

Readme

@matthesketh/ink-stable-state

A useState replacement for Ink apps that skips re-renders when the new value is structurally equal to the old one. Kills the flicker you get when a polling hook calls setState every 10 seconds with identical data.

Install

npm install @matthesketh/ink-stable-state

The problem

A common Ink pattern: fetch some data on a timer, dump it into state, render a list.

interface HealthRow { app: string; ok: boolean }

function useHealth() {
  const [data, setData] = useState<HealthRow[]>([]);
  useInterval(() => {
    fetch('/health').then(setData);  // ← flicker: setState even when nothing changed
  }, 10_000);
  return data;
}

React doesn't know your two arrays are structurally equal — it sees a new reference and re-renders. On a terminal that means a visible repaint every 10 seconds even when nothing changed.

The fix

import { useStableState } from '@matthesketh/ink-stable-state';

function useHealth() {
  const [data, setData] = useStableState<HealthRow[]>([]);
  useInterval(() => {
    fetch('/health').then(setData);  // ← same value? skipped. Different? re-render.
  }, 10_000);
  return data;
}

That's it. Drop-in for useState, identical signature, works with functional updaters too.

API

useStableState<T>(initial, isEqual?)

const [value, setValue] = useStableState<T>(initial: T, isEqual?: (a: T, b: T) => boolean);

| Arg | Type | Default | Description | |-----|------|---------|-------------| | initial | T | — | Initial value. Eager — pass a thunk only if you want lazy init via useState(() => …)'s separate API. | | isEqual | (a, b) => boolean | defaultEquals | Equality function. Returns true when the two values should be treated as the same. |

Returns [value, setValue] — exactly like useState. setValue accepts either a value or a functional updater.

defaultEquals<T>(a, b)

JSON-stringify comparison with the following semantics:

  • Object.is short-circuit for identical references and primitives.
  • null / undefined handled correctly (asymmetric → not equal).
  • JSON.stringify for everything else.
  • Cyclic structures: JSON.stringify throws → defaultEquals returns false → every update is treated as a change (no memoisation benefit, but correctness is preserved). Pass a custom isEqual if you care about memoising cyclic data.
  • Map, Set, RegExp, class instances: JSON.stringify doesn't faithfully represent these types. Two structurally different Maps both stringify to "{}" and would be incorrectly treated as equal — meaning the UI would silently miss the update. Always pass a custom isEqual for these types.
  • Date: ISO-string serialised, so two Dates pointing at the same millisecond compare equal. Different millisecond → not equal. Works as expected.

Exported separately so you can compose:

import { defaultEquals } from '@matthesketh/ink-stable-state';

const equalIgnoringTimestamp = (a, b) =>
  defaultEquals({ ...a, ts: 0 }, { ...b, ts: 0 });

When to use it

  • ✅ Polling hooks that re-fetch identical data on a timer.
  • ✅ Subscription handlers where the source emits redundant updates.
  • ✅ Any place you'd reach for useMemo + useState together to dodge re-renders.

When NOT to use it (or use with a custom isEqual)

  • ❌ State that changes every render anyway (e.g. animation frames). Equality check overhead is wasted.
  • State containing Map, Set, RegExp, class instances, or functionsJSON.stringify doesn't faithfully represent these. Without a custom isEqual, defaultEquals will give wrong answers (treating distinct values as equal and dropping updates).
  • ❌ Cyclic structures — they bypass memoisation entirely (always re-render). Pass a custom isEqual if you need memoisation of cyclic data.
  • ❌ Massive payloads (multi-MB) where stringify cost exceeds re-render cost. Use a custom shallow isEqual instead.

Mutation warning

Treat values as immutable. If you mutate an array/object in place and pass the same reference to setValue, the Object.is short-circuit will fire, the mutation will be lost from React's perspective, and your UI will not update. Always create a fresh value:

// ❌ silently dropped — same reference, Object.is = true
const items = state;
items.push(newRow);
setItems(items);

// ✅ new reference — actually compared and rendered
setItems([...state, newRow]);

License

MIT