tracked-instance
April 3, 2026 · View on GitHub
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 directly — changedData 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>
| Option | Type | Description |
|---|---|---|
equals | (a: unknown, b: unknown) => boolean | Custom equality for primitive leaf values. Replaces ===. |
| Return | Type | Description |
|---|---|---|
data | Ref<Data> | Reactive reference to current data. Mutate directly. |
changedData | Ref<DeepPartial<Data>> | Only modified fields. undefined when nothing has changed. |
isDirty | Ref<boolean> | true when any field differs from the original. |
loadData(newData) | void | Replace data and clear dirty state (new baseline). |
reset() | void | Revert all changes back to the last loadData() baseline. |
useCollection(createItemMeta?, options?)
useCollection<Item, Meta>(
createItemMeta ? : (instance: TrackedInstance<Item>) => Meta,
options ? : TrackedInstanceOptions,
)
:
Collection<Item, Meta>
| Return | Type | Description |
|---|---|---|
items | Ref<CollectionItem[]> | Reactive array of collection items. |
isDirty | ComputedRef<boolean> | true if any item is dirty, new, or soft-removed. |
add(item, index?) | CollectionItem | Add a new item. Appended to end by default. |
remove(index, isHardRemove?) | void | Soft-remove by default. Pass true to splice from array. |
loadData(items) | void | Replace all items and clear dirty state. |
reset() | void | Remove 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
}