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.4.0

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, useMutableState } from "opshot";

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

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

		// ignore() on a value in the factory argument makes the edge at that path untracked.
		element: ignore(new Audio()),

		// unsafeTrack() on a value in the factory argument disables strict at and under that path.
		queue: unsafeTrack(new Playlist()),

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

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

			this.position = position;
		},
	});

	// ...
};

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 at a dangerous edge, at the cause.

Use ignore on a value in the factory argument to make the edge at that path untracked. Use unsafeTrack on a value in the factory argument to disable strict at and under that path.

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, useMutableState } from "opshot";

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, useMutableState } from "opshot";

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

	useEffect(
		() =>
			subscribe(counter, (ops, meta) => {
				// ops: [{
				//   do:   { verb: "assign", path: ["count"], value: 1 },
				//   undo: { verb: "assign", path: ["count"], value: 0 },
				// }]
				// meta: whatever the writer passed, or undefined for bare writes
			}),
		[counter],
	);

	// ...
};

Ops

An op is an invertible pair of halves. Every half uses one of three verbs:

type OperationPath = ReadonlyArray<string | number>;

interface Operation {
	readonly do:
		| {
				readonly verb: "assign";
				readonly path: OperationPath;
				readonly value: unknown;
				readonly ids?: ReadonlyArray<number>;
		  }
		| { readonly verb: "delete"; readonly path: OperationPath }
		| { readonly verb: "link"; readonly path: OperationPath; readonly ref: number };
	readonly undo: Operation["do"];
}

Ids vend in admission-walk order over the emitted artifact; a departure's undo assign may carry ids to rebind that walk, the one naming fact construction cannot re-derive. applyOperations puts them back on a state, so a history is a list of ops and an undo is applyOperations with "undo".

import { useEffect, useRef } from "react";
import { applyOperations, subscribe, useMutableState, type Operation } from "opshot";

const replay = {};

const Counter = () => {
	const counter = useMutableState({ count: 0 });
	const history = useRef<Array<ReadonlyArray<Operation>>>([]);

	useEffect(
		() =>
			subscribe(counter, (ops, meta) => {
				// Skip our own replays, so undo doesn't record itself.
				if (meta === replay) return;

				history.current.push(ops);
			}),
		[counter],
	);

	const undo = () => {
		const ops = history.current.pop();

		if (!ops) return;

		applyOperations(counter, ops, "undo", replay);
	};

	return (
		<>
			<button onClick={() => counter.count++}>+</button>
			<button onClick={undo}>Undo</button>
		</>
	);
};

Replay is exact for anything opshot can see: plain data. State behind a constraint is the exception.

If your state is JSON serializable, then ops are too.

Groups

A group creates states and hears every op from the states it created: one stream for history, sync, persistence, etc.

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

const Editor = () => {
	const group = useGroup();

	// Created through the group, so their ops reach the group's subscribers.
	const doc = useMutableState({ items: new Array<string>() }, { group });
	const selection = useMutableState({ index: 0 }, { group });

	useEffect(
		() =>
			// Fires for doc, selection, and every other state the group created.
			// state is whichever one changed.
			subscribe(group, (state, ops, meta) => {
				// ...
			}),
		[group],
	);

	// ...
};

Channels

A channel binds transact, subscribe, and applyOperations to a typed meta convention, so a listener can tell its own writes from everyone else's.

import { useEffect } from "react";
import { createChannel, useMutableState } from "opshot";

interface DocumentMeta {
	replay?: boolean;
	source?: string;
}

const docChannel = createChannel<DocumentMeta>({ source: "editor" }); // set defaults

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

	useEffect(
		() =>
			docChannel.subscribe(doc, (ops, context) => {
				// A bare write, or a transact from another channel: meta is unknown.
				if (!context.isTransaction) return;

				// Own-channel transaction: meta is typed, with defaults merged.
				if (context.meta.replay) return;

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

	const rename = () => {
		docChannel.transact(doc, () => {
			doc.title = "Draft";
		});
	};

	// ...
};

License

MIT