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

opshot

v0.6.1

Published

Plain-object state for React: mutate it directly, re-render only the components that read what changed, and track every change operation

Readme

opshot

Mutable state for React, with re-render for only the components that read what changed. (It's like valtio, but not a footgun.)

Install

npm install opshot

Mutable state

React state is immutable: changing one field means spreading the old object into a new one.

const [user, setUser] = useState({ name: "Ada", age: 36 });

setUser((prev) => ({ ...prev, age: 37 }));

opshot state is a live mutable object: you assign the field.

const user = useMutableState({ name: "Ada", age: 36 });

user.age = 37;

Bounded re-renders

React re-renders a component and its children when its state changes.

interface User {
	name: string;
	age: number;
}

const Parent = () => {
	const [user, setUser] = useState<User>({ name: "Ada", age: 36 });

	const birthday = () => setUser((prev) => ({ ...prev, age: prev.age + 1 }));

	// A click re-renders Parent and Child.
	return (
		<>
			<button onClick={birthday}>+</button>
			<Child user={user} />
		</>
	);
};

const Child = ({ user }: { user: User }) => <p>{user.age}</p>;

opshot re-renders only what read the change. Wrap a child in scope and it subscribes to the fields it reads. Where the mutation happens doesn't matter — here Parent writes, and only Child re-renders, because renders follow reads, not writes.

const Parent = () => {
	const user = useMutableState<User>({ name: "Ada", age: 36 });

	const birthday = () => {
		user.age++;
	};

	// A click re-renders only Child.
	return (
		<>
			<button onClick={birthday}>+</button>
			<Child user={user} />
		</>
	);
};

const Child = scope<{ user: User }>(({ user }) => <p>{user.age}</p>);

This is how you optimize re-rendering across your component tree: place scope boundaries where you want re-renders contained, and each boundary re-renders only when a field it read changes. useMutableState is a boundary itself.

Creating State

import { ignore, unsafeTrack } from "opshot";
import { useMutableState } from "opshot/react";

interface PlayerState {
	position: number;
	element: HTMLAudioElement;
	queue: Playlist;
	seek: (position: number) => void;
}

const Player = () => {
	const player: PlayerState = useMutableState({
		position: 0,

		// ignore() stores it as-is; opshot never looks inside.
		element: ignore(new Audio()),

		// unsafeTrack() takes it anyway, tracking the plain data on it.
		queue: unsafeTrack(new Playlist()),

		seek(position: number) {
			this.element.currentTime = position;

			if (this.position === position) return;

			this.position = position;
		},
	});

	// ...
};

createMutableState(properties, options) creates the same state outside a component, for a store or a process with no React.

ignore(value, false) and unsafeTrack(value, false) undo these effects on the value.

Constraints

opshot tracks plain data.

It can't track:

  • Hidden stores (language-level features like in Map)
  • #private fields
  • Own function properties on class instances
  • Non-writable properties that hold an object

strict: true throws when opshot meets one of these, naming the value that caused it; strict: false stores the value untracked. It defaults to true in development and false in production.

Tracked collections

TrackedMap, TrackedSet, and TrackedDate stand in for the built-ins opshot rejects. They have the exact same API as their counterparts.

import { TrackedMap } from "opshot";
import { useMutableState } from "opshot/react";

const state = useMutableState({ index: new TrackedMap<string, number>() });

state.index.set("a", 1);

Subscribe

subscribe hears every change to a state.

import { useEffect } from "react";
import { subscribe } from "opshot";
import { useMutableState } from "opshot/react";

interface Meta {
	source: string;
}

const Counter = () => {
	const counter = useMutableState({ count: 0 });

	useEffect(
		() =>
			subscribe<Meta | undefined>(counter, (operations) => {
				// operations: [{ kind: "change", node, key: "count", before: 0, after: 1, meta: undefined }]
			}),
		[counter],
	);

	// ...
};

Operations

An operation is one key's change on one node:

interface AddOperation<Meta = unknown> {
	readonly kind: "add";
	readonly node: Record<string, unknown>; // The live state being applied to
	readonly key: string;
	readonly after: unknown;
	readonly meta: Meta;
}

interface ChangeOperation<Meta = unknown> {
	readonly kind: "change";
	readonly node: Record<string, unknown>;
	readonly key: string;
	readonly before: unknown;
	readonly after: unknown;
	readonly meta: Meta;
}

interface DeleteOperation<Meta = unknown> {
	readonly kind: "delete";
	readonly node: Record<string, unknown>;
	readonly key: string;
	readonly before: unknown;
	readonly meta: Meta;
}

type Operation<Meta = unknown> = AddOperation<Meta> | ChangeOperation<Meta> | DeleteOperation<Meta>;
const revert = (operation: Operation) => {
	if (operation.kind === "add") delete operation.node[operation.key];
	else operation.node[operation.key] = operation.before;
};

const apply = (operation: Operation) => {
	if (operation.kind === "delete") delete operation.node[operation.key];
	else operation.node[operation.key] = operation.after;
};

Undo and redo are a switch on kind. Revert an emission in reverse and apply it forward.

Emission

A state gathers its writes and delivers them together, in order. The window is a microtask by default, so everything you change in one go arrives as one emission carrying the net change — a listener hears where a field ended up, not every step it took there.

emitOn sets the window instead. opshot hands you a flush, and the state waits until you call it.

import { useEffect } from "react";
import { subscribe } from "opshot";
import { useMutableState } from "opshot/react";

const Chart = () => {
	// One emission per frame, however many writes land in between.
	const cursor = useMutableState({ x: 0, y: 0 }, { emitOn: (flush) => requestAnimationFrame(flush) });

	useEffect(
		() =>
			subscribe(cursor, (operations) => {
				// ...
			}),
		[cursor],
	);

	// ...
};

Separate from that callback, the flush(state, ...states) export ends each state's window from outside.

Batches

batch runs a callback and tags every write inside it with your meta, so a listener can tell its own writes from everyone else's.

import { useEffect } from "react";
import { batch, subscribe } from "opshot";
import { useMutableState } from "opshot/react";

const TitleBar = () => {
	const doc = useMutableState({ title: "Untitled" });

	useEffect(
		() =>
			subscribe(doc, (operations) => {
				const edits = operations.filter((operation) => operation.meta !== "replay");

				// ...
			}),
		[doc],
	);

	const rename = () => {
		batch(() => {
			doc.title = "Draft";
		}, "editor");
	};

	// ...
};

Identity

const object = { name: "Ada", age: 36 };
const state = useMutableState(object);

object === state; // false
isSameIdentity(object, state); // true

isState(object); // false
isState(state); // true
// The state's identity changes when the state or anything nested inside it changes.
useEffect(() => {
	// ...
}, [state]);

License

MIT