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

optimistic-colada

v0.1.0

Published

Declarative optimistic updates plugin for Pinia Colada

Readme

optimistic-colada

Declarative optimistic updates for Pinia Colada.

A Pinia Colada plugin that snapshots the query cache when a mutation starts, writes the expected result immediately, rolls the cache back if the mutation fails, and invalidates the affected queries once it settles. All of it is configured with a single optimistic option on useMutation instead of hand-written onMutate / onError / onSettled hooks in every mutation.

Why

The manual pattern for optimistic updates looks like this, repeated in every mutation:

const queryCache = useQueryCache()

const { mutate: renameProject } = useMutation({
  mutation: (input: { id: string; name: string }) => api.projects.rename(input),
  onMutate(vars) {
    const previous = queryCache.getQueryData<Project[]>(['projects'])
    queryCache.setQueryData(
      ['projects'],
      previous?.map((p) => (p.id === vars.id ? { ...p, ...vars } : p))
    )
    return { previous }
  },
  onError(_error, _vars, context) {
    queryCache.setQueryData(['projects'], context.previous)
  },
  onSettled() {
    queryCache.invalidateQueries({ key: ['projects'] })
  }
})

This version handles exactly one query key. If the same record is cached under several keys — a list, a filtered list, a detail view — each one needs its own snapshot and rollback.

With the plugin:

const { mutate: renameProject } = useMutation({
  mutation: (input: { id: string; name: string }) => api.projects.rename(input),
  optimistic: {
    action: OptimisticAction.update,
    key: ['projects']
  }
})

The plugin applies the update to every cache entry under ['projects'] (prefix match), snapshots each one for rollback, and invalidates them after the mutation settles.

Requirements

  • vue >= 3.5
  • @pinia/colada >= 1.3

Installation

Not published to npm yet; install from GitHub:

npm install github:Prains/optimistic-colada

The package builds itself on install via the prepare script.

Setup

Register the plugin in Pinia Colada options.

Vue:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { PiniaColada } from '@pinia/colada'
import { createOptimisticPlugin } from 'optimistic-colada'

const app = createApp(App)
app.use(createPinia())
app.use(PiniaColada, {
  plugins: [createOptimisticPlugin()]
})

Nuxt (with @pinia/colada-nuxt):

// colada.options.ts
import type { PiniaColadaOptions } from '@pinia/colada'
import { createOptimisticPlugin } from 'optimistic-colada'

export default {
  plugins: [createOptimisticPlugin()]
} satisfies PiniaColadaOptions

Usage

Update

import { useMutation } from '@pinia/colada'
import { OptimisticAction } from 'optimistic-colada'

const { mutate: renameProject } = useMutation({
  mutation: (input: { id: string; name: string }) => api.projects.rename(input),
  optimistic: {
    action: OptimisticAction.update,
    key: ['projects']
  }
})

On renameProject({ id, name }), every cached query whose key starts with ['projects'] is scanned for records whose id matches the mutation variables. Matched records are shallow-merged with the variables. If the request fails, the previous data is restored. When the mutation settles, queries under ['projects'] are invalidated, so active queries refetch and the cache converges to server state.

Delete

const { mutate: deleteProject } = useMutation({
  mutation: (input: { id: string }) => api.projects.delete(input),
  optimistic: {
    action: OptimisticAction.delete,
    key: ['projects']
  }
})

Matching records are removed from arrays and paginated results. In paginated shapes, total is decremented by the number of removed records.

Create

There is no built-in updater for create — the shape of a new record cannot be inferred from mutation variables. Provide one:

import { OptimisticAction, createOptimisticId } from 'optimistic-colada'

const { mutate: createTask } = useMutation({
  mutation: (input: { title: string }) => api.tasks.create(input),
  optimistic: {
    action: OptimisticAction.create,
    key: ['tasks'],
    updater: (cached, input) => [
      ...(cached as Task[]),
      { id: createOptimisticId(), title: input.title, done: false }
    ]
  }
})

createOptimisticId() returns a prefixed UUID for the placeholder record, and isOptimisticId(id) lets components tell placeholders from persisted records — for example, to disable row actions until the record is real. The invalidation after settle replaces placeholders with server data.

Custom updater

An updater overrides the built-in behavior for any action. It is called once per matching cache entry with the current cached data and the mutation variables, and must return the new data without mutating the input. Return undefined or the same reference to leave an entry untouched.

optimistic: {
  action: OptimisticAction.update,
  key: ['tasks'],
  updater: (cached, input) => {
    if (!Array.isArray(cached)) return undefined // skip this entry
    return cached.map((t) => (t.id === input.id ? { ...t, done: input.done } : t))
  }
}

Skipping invalidation

optimistic: {
  action: OptimisticAction.update,
  key: ['settings'],
  skipInvalidation: true
}

By default, queries under key are invalidated after a successful mutation. Set skipInvalidation: true when the optimistic write already is the final state and a refetch adds nothing. On error, rollback and invalidation happen regardless of this flag.

Identity key

Built-in updaters match records by id. If your records use a different field:

createOptimisticPlugin({ identityKey: 'uuid' })

Supported cache shapes

Built-in updaters detect the shape of each cache entry independently:

| Shape | Example | update | delete | | --------- | ---------------------------- | ------------------------------------------- | ------------------------------------------ | | Array | [{ id, ... }, ...] | shallow-merge variables into matched records | remove matched records | | Paginated | { items: [...], total? } | same, inside items | remove from items, decrement total | | Single | { id, ... } | shallow-merge when id matches | skipped |

Entries with any other shape are skipped, with a console warning in dev builds. Entries that have no data yet are skipped silently.

How it works

  1. On mutate, the plugin reads the optimistic config from the mutation options and collects every cache entry matching key (prefix match, same semantics as queryCache.getEntries).
  2. For each entry it computes the optimistic data — via updater if provided, otherwise via the built-in updater for the action — takes a snapshot of the current data (deep clone, aware of arrays, plain objects, Date, Map, Set), and writes the optimistic data with setQueryData.
  3. On success, queries under key are invalidated (unless skipInvalidation), so active queries refetch and optimistic data is replaced with server state.
  4. On error, snapshots are restored and queries under key are invalidated to resynchronize with the server.

API

createOptimisticPlugin(options?): PiniaColadaPlugin

| Option | Type | Default | Description | | ------------- | -------- | ------- | -------------------------------------------------- | | identityKey | string | 'id' | Field used by built-in updaters to match records |

The optimistic mutation option

The package augments UseMutationOptions, so once it is imported anywhere in the project, optimistic is available and typed on every useMutation call.

| Field | Type | Required | Description | | ------------------ | ------------------------------------------- | -------------- | ----------------------------------------------------------------- | | action | OptimisticAction.create \| update \| delete | yes | Selects the built-in updater | | key | EntryKey | yes | Query key prefix; all matching entries are updated | | updater | (cachedData, vars) => unknown | for create | Custom per-entry transform, overrides the built-in updater | | skipInvalidation | boolean | no | Skip query invalidation after a successful mutation |

Standalone updaters

applyUpdate(data, input, identityKey), applyDelete(data, input, identityKey) and detectShape(data) are exported for reuse inside custom updaters and in tests.

Optimistic IDs

createOptimisticId(), isOptimisticId(id), OPTIMISTIC_PREFIX — helpers for placeholder records in create flows.

Behavior notes

  • Built-in updaters require the mutation variables to be a plain object carrying the identity key, e.g. { id, ...patch }. With non-object variables and no updater, the mutation runs without an optimistic write.
  • update shallow-merges the full variables object into matched records. Variables that are not record fields (flags, nested filter params) will show up in the cached record until invalidation refetches it; use a custom updater in that case.
  • Rollback restores per-entry snapshots taken when the mutation started. If several mutations touch the same entries concurrently, a rollback can briefly restore data from before a neighboring mutation — the invalidation that follows brings the entries back to server state.
  • Mutations without an optimistic config are ignored by the plugin.

Development

npm install
npm test        # vitest
npm run typecheck
npm run build   # tsup -> dist/

License

MIT