Model the app's data with @data-client schemas, which normalize responses into one shared cache so an update shows everywhere. Use it whenever an endpoint returning data is added or its response changes (resource, RestEndpoint, GraphQL, Endpoint), when the data's shape changes, when deriving views from loaded data, or when one item looks different in two places. Covers Entity, GQLEntity, Collection, Union and Query.
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Compose schemas to match the JSON an endpoint returns. Import them from the protocol package
the app uses (@data-client/rest, @data-client/graphql or @data-client/endpoint; both protocol packages re-export
endpoint). For GraphQL, extend GQLEntity instead of Entity; it declares the id field used as pk().
{[key:string]: Schema} - immutable objects[Schema] - immutable listsnew Collection(Values(Schema)) - mutable/growable mapsnew Query(Queryable) - memoized programmatic selectors
const queryRemainingTodos = new Query(
TodoResource.getList.schema,
entries => entries.filter(todo => !todo.completed).length,
);Define Query transformations with the data model (e.g. src/resources/) — not inside custom hooks
wrapping useSuspense/useQuery, which hides data dependencies and couples data logic to view code.
Entity subclass defines defaults for all non-optional serialised fields: bare TS field types emit no
runtime value, so schema inference breaks.pk() only when the primary key ≠ id.static indexes = ['username' as const] to look up by a unique non-pk field with useQuery(User, { username }), instead of building lookup Mapspk() return type is number | string | undefinedEntity.process(value, parent, key, args) to adapt input before normalizing, e.g. add fields from
args/url, or turn an API's stand-in for a missing nested entity (like {}) into undefined
(example)static schema (optional) for nested schemas or deserialization functions; it also joins related entities
client-side, so never merge or 'enrich' data by hand (relational-data)
process() → pk() → validate() → visit nested schemas (recurse into schema fields) → if existing: mergeWithStore() which calls shouldUpdate() and maybe shouldReorder() + merge(); metadata via mergeMetaWithStore().shouldReorder(existingMeta, incomingMeta, existing, incoming) to compare a version field such as updatedAt; that alone keeps the newer fields. shouldUpdate() is for skipping updates to save work.fromJS(), restoring prototype chain so getters, methods, and schema processing work. Order: createIfValid() → validate() → fromJS() → unvisit nested schemas (recurse into schema fields).To define polymorphic resources (e.g., events), use Union and a discriminator field.
import { Union } from '@data-client/rest';
export abstract class Event extends Entity {
type: EventType = 'Issue'; // discriminator field is shared
/* ... */
}
export class PullRequestEvent extends Event { /* ... */ }
export class IssuesEvent extends Event { /* ... */ }
export const EventResource = resource({
path: '/users/:login/events/public/:id',
schema: new Union(
{
PullRequestEvent,
IssuesEvent,
// ...other event types...
},
'type', // discriminator field
),
});Collections wrap Array or Values schemas to enable mutations (add/remove/move).
pk() uses nestKey(parent, key) when nested in an Entity and available; otherwise it uses argsKey(...args), then serializes the result. Without options, it defaults to argsKey: params => ({ ...params }), using all endpoint args as the collection key.
argsKey — derive pk from endpoint arguments (default)nestKey — derive pk from parent entity for nested shared-state collectionsDefine both on the same Collection to reuse one definition top-level and nested. When argsKey(args) and nestKey(parent) produce the same object shape, the top-level fetch and the nested read resolve to the same (referentially equal) array/map — push/unshift/assign/move/remove on either updates both:
const userTodos = new Collection([Todo], {
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
nestKey: (parent: User) => ({ userId: parent.id }),
});Default createCollectionFilter uses nonFilterArgumentKeys (default: keys starting with 'order') to exclude non-filter args when matching collections. This affects which existing collections receive new items from push/unshift/assign/move: pushing with { userId: 1 } reaches the {} and { userId: 1 } lists, not { userId: 2 }, and a list arg left out of nonFilterArgumentKeys (such as a sort) keeps new items out of that list.
Override as function, RegExp, or string[]:
new Collection([Todo], { nonFilterArgumentKeys: /orderBy|sortDir/ })A plain fetch of a Collection endpoint replaces its contents; only the extenders below and
getPage add to it, so load more pages with getPage.
Use each as an endpoint's schema (network; resource() exposes them as getList.push etc.) or with ctrl.set()
(local-only).
| Method | Type | Description |
|---|---|---|
push | Array | Entity | Append items to end |
unshift | Array | Entity | Prepend items to start |
assign | Values | Merge entries into map |
remove | Both | Remove items by value from matching collections |
move | Both | Remove from collections matching existing state, add to collections matching new state |
addWith(merge, filter?) | Both | Custom creation schema (used internally by push/unshift/assign) |
moveWith(merge) | Both | Custom move schema (control insertion order, e.g., unshift merge for prepending) |
When an endpoint returns partial or differently-shaped data for an entity already in cache (e.g., a metadata endpoint, a stats endpoint, a lazy-load expansion endpoint), use the same Entity as the schema — don't create a wrapper entity.
See partial-entities for patterns and examples.
schema bypasses normalization: its data is never updated by other endpoints' responses or
ctrl.set().Denormalize<typeof schema>, not InstanceType<>; it handles every schema (Union, Collection),
not just Entity.Vue projects: read <name>.vue.md instead of <name>.md when it exists.
reactive/data-client
e1ea9ce
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.