Project Awesome project awesome

UI Utilities > tracked-instance

Build large forms and track all changes.

Package 5 stars GitHub

tracked-instance

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>
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
}
Back to Vue.js