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

patchfork

v0.0.1

Published

A clever (derogatory) immutable state utility for TypeScript.

Downloads

6

Readme

patchfork

A tiny, fast, and clever (derogatory) immutable state utility for TypeScript.

npm install patchfork

Basic Usage

patchfork has two core functions: patch and fork.

fork

The fork function 'forks' an immutable object, creating a new version with changes applied.

import { fork } from 'patchfork'

fork({ settings: { theme: 'light' } }).settings.theme('dark')
// => { settings: { theme: 'dark' } }

// Pass a function to update a value
fork({ user: { name: 'Nick Cave' } }).user.name(
  (name) => name.toUpperCase() + '!!!',
)
// => { user: { name: 'NICK CAVE!!!' } }

// Array, Map, and Set methods work as expected.
fork({ nums: [1, 2, 3] }).nums.push(4)
// => { nums: [1, 2, 3, 4] }

So it's kinda like Immer's produce function, but with a wacky API optimized for one-liners.

patch

patch has the same interface as fork, but it operates on a state container to immutably update it.

Any state container with 'get' and 'set' operations can be adapted to work with patch. We provide minimal adapters for React useState, Zustand, and Jotai (PRs welcome for others!).

import { patch } from 'patchfork'
import { usePatchableState } from 'patchfork/react'

function App() {
  const [state, store] = usePatchableState({
    todos: [
      { text: 'Buy milk', completed: false },
      { text: 'Buy eggs', completed: false },
    ],
  })
  const addTodo = (text: string) => {
    const text = window.prompt('Enter a todo')
    if (text) {
      patch(store).todos.push({ text, completed: false })
    }
  }
  const toggleTodo = (index: number) => {
    patch(store).todos[index].completed((completed) => !completed)
  }

  return (
    <div>
      <button onClick={() => addTodo()}>Add</button>
      <ul>
        {state.todos.map((todo, i) => (
          <li key={i} onClick={() => toggleTodo(i)}>
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  )
}

Batching operations

Use fork.do or patch.do to batch operations efficiently.

import { fork, patch } from 'patchfork'

const nextState = fork.do({ user: { name: 'Nick Cave', age: 50 } }, (state) => {
  // `state` is a shallow clone of the input object,
  // i.e. it's a regular JS object, not a Proxy.
  console.log(state) // { user: { name: 'Nick Cave', age: 50 } }
  // Call `patch(state)` to make changes.
  patch(state).user.name('Nicholas Cage')
  patch(state).user.age(51)

  // TypeScript will prevent you from doing unsafe mutation.
  // ❌ Error: Property 'age' is readonly
  state.user.age = 52
  // ❌ Error: state.user not patchable
  patch(state.user).name('Nicholas Cage')
})

Optional chaining

fork and patch always do optional chaining. It works just like JavaScript optional chaining. i.e. the whole expression will evaluate to undefined if the operation can't be performed.

import { fork } from 'patchfork'

interface User {
  name?: string
  settings?: Map<string, string>
}
const user: User = {}

// 'assignment' operations on optional properties always execute.
fork(user).name('Joe')
// => { name: 'Joe' }
// TS type: User

// 'update' operations on optional properties will only execute if the property is not undefined.
fork(user).name((name) => name.toUpperCase())
// => undefined
// TS type: User | undefined

// Collection methods will only succeed if the collection is not undefined or null.
fork(user).settings.set('theme', 'dark')
// => undefined
// TS type: User | undefined

You can use the ?? operator to perform a fallback operation if the first fails.

const y =
  fork(user).name((name) => name.toUpperCase()) ?? fork(user).name('default')
// => { name: 'default' }
// TS type: User

Updating values inside Maps

Maps allow keys of any type, so they need a separate syntax.

Use the special key symbol followed by the key itself in parentheses to operate on Map values.

import { key, fork } from 'patchfork'
const state = {
  users: new Map([['user1', { name: 'John', age: 30 }]]),
}

const nextState = fork(state).users[key]('user1').name('Wilberforce')
// => { users: new Map([['user1', { name: 'Wilberforce', age: 30 }]]) }

Async operations

patch operations on AsyncPatchable (e.g. the React useState adapter) stores will always return a promise.

Additionally, patch.do and fork.do will return a promise if the callback is async.

If you are checking whether the return value is undefined to perform a fallback operation, remember to await the promise.

const updateTodo = async (title: string) => {
  ;(await patch.do(store).todos[0].title(title)) ??
    patch.do(store).todos.push({ title, completed: false })
}

Advanced Usage

You can also use fork.do and patch.do on nested paths.

import { fork, patch } from 'patchfork'

const state = {
  user: {
    name: 'Nick Cave',
    settings: {
      theme: 'light',
      fontSize: 16,
    },
  },
}
const nextState = fork.do(state).user.settings((settings) => {
  patch(settings).theme('dark')
  patch(settings).fontSize(20)
})

Freezing objects at development time

TypeScript should prevent unsafely mutating data within patchfork's draft functions if your data is well-typed and you don't use as any ! But who knows what might happen later, in the outside world. Shit's crazy out there.

For extra peace of mind, call setDevMode(true) early in your application's boot process to freeze objects at development time.

This will cause errors to be thrown if you try to mutate an object that is supposed to be immutable.

import { setDevMode } from 'patchfork'
if (process.env.NODE_ENV === 'development') {
  setDevMode(true)
}

Custom State Container Integration

See Custom State Container Integration docs for implementation details and examples.

Performance

patchfork seems to be:

  • About 5x faster than immer's production mode.
  • About 3x faster than mutative (same API as immer but highly optimized)

The benchmarks could be more thorough so take this for what it's worth.

https://github.com/ds300/patchfork/tree/main/bench

Limitations

  • 🩹 No support for patch generation/application.

  • 👭 It currently only with data supported by structuredClone (So yes ✅ to Map, Set, plain objects, and arrays. And no ❌ to custom classes, objects with symbol keys or getters/setters, etc)

  • It currently returns a new object even if an edit is ineffectual, e.g.

    const foo = { bar: 'baz' }
    const nextState = edit(foo).bar('baz')
    newState !== foo // sadly true

    This could be fixed partially for certain usage patterns (PRs welcome).