tracked-instance

April 3, 2026 · View on GitHub

Version

Track form changes in Vue 3 and send only modified fields to the backend — no more diffing payloads by hand.

const {data, changedData, isDirty} = useTrackedInstance({name: 'Jack', age: 30})

data.value.name = 'John'

changedData.value  // { name: 'John' }   ← only what changed
isDirty.value      // true

data.value.name = 'Jack'  // revert
changedData.value  // undefined           ← back to clean
isDirty.value      // false

Install

npm i tracked-instance

Supports Vue 3 only.


useTrackedInstance  ·  ▶ Try on playground

Track changes to a single object, primitive, or array.

import {useTrackedInstance} from 'tracked-instance'

const {data, changedData, isDirty, loadData, reset} = useTrackedInstance({
  name: 'Jack',
  isActive: false,
})

Mutate data.value directlychangedData and isDirty update automatically:

data.value.name = 'John'
isDirty.value      // true
changedData.value  // { name: 'John' }

// Revert to original value → field disappears from changedData
data.value.name = 'Jack'
isDirty.value      // false
changedData.value  // undefined

reset() — revert all changes back to the last loaded baseline:

data.value.name = 'John'
reset()
data.value  // { name: 'Jack', isActive: false }

loadData(newData) — replace data without marking anything dirty (use after a successful save):

loadData({name: 'Joe', isActive: true})
isDirty.value  // false  ← Joe is now the new baseline

Works with primitives and arrays too:

useTrackedInstance(false)
useTrackedInstance([1, 2, 3])

Custom equality with equals

By default values are compared with ===. Override this for edge cases — for example when a UI component writes null but the backend sends "":

const {data, isDirty} = useTrackedInstance(
  {comment: null},
  {equals: (a, b) => (a ?? '') === (b ?? '')}
)

data.value.comment = ''     // treated as equal to null
isDirty.value               // false

data.value.comment = 'hi'
isDirty.value               // true

useCollection  ·  ▶ Try on playground

Track an array of items — add, remove, modify, and reset the whole list.

import {useCollection} from 'tracked-instance'

const {items, isDirty, add, remove, loadData, reset} = useCollection()

loadData([{name: 'Jack'}, {name: 'John'}, {name: 'Joe'}])

Each item in items is a CollectionItem with its own TrackedInstance:

items.value[0].instance.data.value.name = 'Stepan'
isDirty.value  // true

add(item, index?) — add a new item (marked isNew: true):

const newItem = add({name: 'Taras'})
// newItem.isNew.value === true
// newItem.isRemoved.value === false

add({name: 'Taras'}, 0)  // insert at position 0

remove(index, isHardRemove?) — soft-delete by default, hard-delete with true:

remove(0)        // soft remove: isRemoved = true, item stays in array
remove(0, true)  // hard remove: spliced out immediately

Soft-removed items can be restored with reset() or by setting isRemoved.value = false manually.

reset() — removes new items, restores soft-removed ones, reverts all changes:

reset()

Item meta

Attach computed or reactive metadata to each item via a factory function:

const {add, items} = useCollection(instance => ({
  isValidName: computed(() => instance.data.value.name.length > 0)
}))

add({name: ''})
items.value[0].meta.isValidName.value  // false

The same options (including equals) are forwarded to every TrackedInstance in the collection:

const {items} = useCollection(
  () => undefined,
  {equals: (a, b) => (a ?? '') === (b ?? '')}
)

API Reference

useTrackedInstance(initialData?, options?)

useTrackedInstance<Data>(initialData ? : Data, options ? : TrackedInstanceOptions)
:
TrackedInstance<Data>
OptionTypeDescription
equals(a: unknown, b: unknown) => booleanCustom equality for primitive leaf values. Replaces ===.
ReturnTypeDescription
dataRef<Data>Reactive reference to current data. Mutate directly.
changedDataRef<DeepPartial<Data>>Only modified fields. undefined when nothing has changed.
isDirtyRef<boolean>true when any field differs from the original.
loadData(newData)voidReplace data and clear dirty state (new baseline).
reset()voidRevert all changes back to the last loadData() baseline.

useCollection(createItemMeta?, options?)

useCollection<Item, Meta>(
  createItemMeta ? : (instance: TrackedInstance<Item>) => Meta,
  options ? : TrackedInstanceOptions,
)
:
Collection<Item, Meta>
ReturnTypeDescription
itemsRef<CollectionItem[]>Reactive array of collection items.
isDirtyComputedRef<boolean>true if any item is dirty, new, or soft-removed.
add(item, index?)CollectionItemAdd a new item. Appended to end by default.
remove(index, isHardRemove?)voidSoft-remove by default. Pass true to splice from array.
loadData(items)voidReplace all items and clear dirty state.
reset()voidRemove new items, restore soft-removed, reset all instance data.

CollectionItem

interface CollectionItem<Item, Meta = undefined> {
  instance: TrackedInstance<Item>                 // tracked instance for this item
  isNew: Ref<boolean>                             // true for items added via add()
  isRemoved: Ref<boolean>                         // true after soft remove
  meta: Meta                                      // custom metadata from createItemMeta()
  remove(isHardRemove?: boolean): void            // shortcut to remove self
}