Skip to content

useStructureDataManagement ​

The base primitive: a normalized { id -> record } store with CRUD, selection state, client-side pagination, and hasMany/belongsTo parent-relation bookkeeping. No networking — useStructureRestApi builds fetching/caching on top of this. Use it directly when you already have the data (e.g. from a WebSocket push, a props tree, a non-REST source) and just need somewhere reactive and CRUD-shaped to put it.

Quickstart ​

ts
import { useStructureDataManagement } from '@guebbit/vue-toolkit'

interface IUser {
    id: number
    name: string
}

// K (the id type) is inferred from IUser['id']: number. Pass it explicitly for a composite id.
const users = useStructureDataManagement<IUser>()

users.addRecord({ id: 1, name: 'Alice' })
users.getRecord(1) // { id: 1, name: 'Alice' }

users.editRecord({ name: 'Alice Updated' }, 1) // merges into the existing record

users.itemList.value // IUser[] — computed view of the whole store

API ​

Setup ​

useStructureDataManagement<T, K, P>(identifiers = 'id', delimiter = '|', recordStore?, relationStore?) returns IStructureDataManagementApi<T, K, P> — an exported, explicit interface (not inferred).

  • T is the record type, K its id type, P a parent's id type. K defaults to the type of T['id'] when T has one (string | number otherwise) — the natural default, since the default identifiers is 'id'. Pass K explicitly for a composite id, or one under a different field name.
  • identifiers is a single field name, or an array of fields for composite keys (order matters); delimiter joins composite key parts into one dictionary key. With two or more identifiers, each part is escaped before joining: every character of delimiter, and the escape character itself, gets the escape character in front. So ['a|b', 'c'] and ['a', 'b|c'] never collide into the same joined key, whatever the delimiter ('::' or '--' included). The escape character is a backslash, unless delimiter contains one — then the next character code it does not use. A single identifier is never escaped (there is nothing to collide with), so the common case stays a plain, readable id.
  • recordStore (type IRecordStore<T, K>) is the write surface addRecord/editRecord/ deleteRecord/setRecords/resetRecords go through. You will not normally pass one: the default is a local, in-memory dictionary. useStructureRestApi passes a TanStack-backed one, which turns the dictionary into a read-only view of the query cache.
  • relationStore (type IRelationStore<P, K>) is the write surface the hasMany/belongsTo methods below go through, the same seam as recordStore but for parent/child links. Same default and same REST override: a local dictionary unless useStructureRestApi passes a TanStack-backed one, so relations live in the same query cache as the records instead of a second, separately-tracked copy nothing outside this composable would ever read.

Every read/write method (addRecord, getListByParent, ...) is the same either way — only what sits behind the seam changes: a plain object under a local ref, or a computed view over the shared QueryClient cache that every other resource on the same client can also invalidate.

IRecordStore memberMeaning
dictionaryRef<Record<K, T>>: the reactive read view.
write(id, item)Stores one record. Whether it counts as freshly fetched is the store's own concern.
remove(id)Removes one record.
writeAll(items)Replaces every record.
clear()Removes every record.
resolve?(id)Optional. Follows id one hop to the id its record actually lives under. getRecord calls it when present, and reads id as given otherwise (the default store has none). The TanStack-backed store uses it so fetchTarget/watchTarget by an alternate key (a slug) reads through to the record's own id instead of a second, divergent copy.
read?(id)Optional. One record by id, without going through dictionary. Both built-in stores implement it (an O(1) plain-object read locally, one getQueryData under the REST layer); editRecord uses it so merging or partially writing a batch never pays for rebuilding the whole reactive dictionary computed on every item.
isFetching?()Optional. True while the write in progress is a server answer, not a caller-driven create (see the REST store's asFetched). Absent on a store with no such distinction (the default one). addRecord/editRecord read it to decide whether to move lastInsertedIdentifier — a record the REST layer stores because the server reported it is not a "just created" record, even though it is the same write path.
IRelationStore memberMeaning
dictionaryRef<Record<P, K[]>>: the reactive read view, parentHasMany below.
addToParent(parentId, childId)Links a child to a parent, once.
removeFromParent(parentId, childId)Unlinks a child from a parent.
removeDuplicateChildren(parentId)Drops repeated child ids of a parent.

Both built-in stores compare child ids the way the record dictionary keys them: 1 and '1' (a route param) are the same child, while a symbol only matches itself.

CRUD ​

Method / propertyPurpose
itemDictionaryThe reactive Record<K, T> of every record (the record store's dictionary).
itemListComputed array view of itemDictionary.
getRecord(...idParts)Look up one record. Accepts variadic parts for composite identifiers (joined internally).
getRecords(idsArray)Look up many records; each entry is a single id or an array of composite parts; misses are dropped.
addRecord(itemData)Insert or overwrite one record. Returns itemData.
addRecords(itemsArray)Bulk addRecord; falsy entries are skipped.
editRecord(data, id?, create = true)Partial merge into an existing record, or creates it when create is true. id (0 and '' included) defaults to the id inside data when create is on; an array of composite parts is joined. Returns the new id if it created a record, undefined if it only updated one.
editRecords(itemsArray)Bulk editRecord; falsy entries are skipped.
deleteRecord(id)Removes a record. Returns true, or undefined when there was none.
setRecords(items)Replaces the whole dictionary. Returns items.
resetRecords()Empties the dictionary.
createIdentifier(itemData, customIdentifiers?)Builds the dictionary key for an item; auto-generates and writes back a fallback id if one is missing.
identifierThe (possibly joined) identifier field name, as a plain string.

Selection & last-inserted tracking ​

PropertyPurpose
selectedIdentifierRef — id of the "currently selected" record (list selection, detail page, ...).
selectedRecordComputed record for selectedIdentifier.
lastInsertedIdentifierRef — id of the last record addRecord stored, or editRecord created (an editRecord that only updates leaves it alone).
lastInsertedIdentifiersRef — ids from the most recent batch call: every record addRecords stored, or every record editRecords created.
lastInsertedRecordComputed record for lastInsertedIdentifier.

Client-side pagination ​

Operates on itemList — for offline/already-fetched data. For server-side pagination, see fetchPaginate on useStructureRestApi.

PropertyPurpose
pageCurrentRef — current page, 1-based.
pageSizeRef — items per page (default 10), always a whole number of at least 1. A write is rounded down, and a value under 1 becomes 1 (0 or negative would make pageTotal read Infinity). A non-finite write (NaN, Infinity) is ignored: the current size stays.
pageTotalComputed — total page count.
pageOffsetComputed — index of the first item on the current page.
pageItemListComputed — itemList slice for the current page.

hasMany / belongsTo relations ​

For child records that need to remember which parent they belong to. Written through relationStore (see Setup above) — under useStructureRestApi, that store is a view over the same query cache the records live in, not a second copy this composable owns:

Method / propertyPurpose
parentHasManyRef<Record<P, K[]>> mapping a parent id to its child ids (a read-only computed view under useStructureRestApi).
addToParent(parentId, childId)Appends a child id under a parent (lazily creates the list); a no-op if it is already there.
removeFromParent(parentId, childId)Removes a child id from a parent's list. '1' removes a linked 1.
removeDuplicateChildren(parentId)Dedupes a parent's child-id list (1 and '1' count as one).
getRecordsByParent(parentId?)Resolves a parent's child ids into a Record<K, T> of full records. 0 and '' are real parent ids; only an omitted (undefined) one returns {}.
getListByParent(parentId?)Same, as an array in the relation's order.

Fallback ids ​

Missing identifiers are filled in with getUuid() from @guebbit/js-toolkit — a random v4 UUID (crypto.randomUUID() when available; otherwise, as in a browser on a non-secure origin, the same shape built from crypto.getRandomValues). The same helper backs useNotificationsStore toast ids. Import it from @guebbit/js-toolkit if you need it yourself.

Gotchas ​

  • Identifier changes don't rekey the dictionary. If a partial editRecord call changes a field that's part of the identifier, the record stays under its old dictionary key — you get an orphaned entry, not a moved one. Delete and re-add if you need to change an id.
  • Missing ids get a fallback, loudly. If you addRecord/editRecord an item without its identifier field(s) set, a random id is generated and written back onto the item — and a console.warn fires so it's visible in dev tools rather than silently wrong.
  • Composite identifiers can be passed as multiple arguments (getRecord('part1', 'part2')), which are joined with delimiter, or as the joined id createIdentifier builds.
  • Nothing here evicts by age. The dictionary is only cleared by resetRecords() (or, on useStructureRestApi, by resetAll(), a dependsOn change or the maxRecords critical-mass wipe). Stale data is treated as useful data, not garbage.

See also ​

Documentation for @guebbit/vue-toolkit v5.2.0. Released under the AGPL-3.0 license.