Skip to main content

Entity and Data Normalization

Entities have a primary key. This enables easy access via a lookup table. This makes it easy to find, update, create, or delete the same data - no matter what endpoint it was used in.

Entities cache

Extracting entities from a response is known as normalization. Accessing a response reverses the process via denormalization.

Global Referential Equality

Using entities expands Reactive Data Client' global referential equality guarantee beyond the granularity of an entire endpoint response.

Mutations and Dynamic Data

When an endpoint changes data, this is known as a side effect. Marking an endpoint with sideEffect: true tells Reactive Data Client that this endpoint is not idempotent, and thus should not be allowed in hooks that may call the endpoint an arbitrary number of times like useSuspense() or useFetch()

By including the changed data in the endpoint's response, Reactive Data Client is able to able to update any entities it extracts by specifying the schema.

import { RestEndpoint, schema } from '@data-client/rest';

const todoCreate = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos',
method: 'POST',
schema: new schema.Collection([Todo]).push,
});
Example Usage
import { useController } from '@data-client/react';

export default function NewTodoForm() {
const ctrl = useController();
return (
<Form
onSubmit={e => ctrl.fetch(todoCreate, new FormData(e.target))}
>
<FormField name="title" />
</Form>
);
}
info

Mutations automatically update the normalized cache, resulting in consistent and fresh data.

Schema

Schemas are a declarative definition of how to process responses

import { RestEndpoint, schema } from '@data-client/rest';

const getTodoList = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos',
schema: new schema.Collection([Todo]),
});

Placing our Entity Todo in an array Collection, allows us to easly push or unshift new Todos on it.

Aside from array, there are a few more 'schemas' provided for various patterns. The first two (Object and Array) have shorthands of using object and array literals.

Data TypeMutableSchemaDescriptionQueryable
ObjectEntitysingle unique object
Union(Entity)polymorphic objects (A | B)
🛑Objectstatically known keys🛑
Invalidate(Entity)delete an entity🛑
ListCollection(Array)growable lists
🛑Arrayimmutable lists🛑
Alllist of all entities of a kind
MapCollection(Values)growable maps
🛑Valuesimmutable maps🛑
anyQuery(Queryable)memoized custom transforms

Learn more

Nesting

Additionally, Entities themselves can specify nested schemas by specifying a static schema member.

import { Entity } from '@data-client/endpoint';

class Todo extends Entity {
id = 0;
user = User.fromJS();
title = '';
completed = false;

static key = 'Todo';

static schema = {
user: User,
};
}

class User extends Entity {
id = 0;
username = '';

static key = 'User';
}

Learn more

Data Representations

Additionally, functions can be used as a schema. This will be called during denormalization. This might be useful with representations like bignumber or temporal instant

import { Entity } from '@data-client/endpoint';

class Todo extends Entity {
id = 0;
user = User.fromJS();
title = '';
completed = false;
dueDate = Temporal.Instant.fromEpochSeconds(0);

static key = 'Todo';

static schema = {
user: User,
dueDate: Temporal.Instant.from,
};
}
info

Due to the global referential equality guarantee - construction of members only occurs once per update.

Store Inspection (debugging)

DevTools browser extension can be installed to inspect and debug the store.

browser-devtools

Data Client Debugging Guide »

Benchmarks

Here we compare denormalization performance with the legacy normalizr library, which has less features, but similar schema definitions.

Memoization is done at every entity level - no matter how nested, ensuring global referential equality guarantees and up to 20x performance even after mutation operations like Create, Update and Delete.

View benchmark