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.
- State
- Response
- Endpoint
- Entity
- React
[
{ "id": 1, "title": "this is an entity" },
{ "id": 2, "title": "this is the second entity" }
]
const PresentationList = new Endpoint(
() => fetch(`/presentations`).then(res => res.json()),
{ schema: [PresentationEntity] },
);
class PresentationEntity extends Entity {
readonly id: string = '';
readonly title: string = '';
pk() {
return this.id;
}
}
export function PresentationsPage() {
const presentation = useSuspense(PresentationList, {});
return presentation.map(presentation => (
<div key={presentation.pk()}>{presentation.title}</div>
));
}
Extracting entities from a response is known as normalization
. Accessing a response reverses
the process via denormalization
.
Using entities expands Rest Hooks' 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 Rest Hooks 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, Rest Hooks is able to able to update any entities it extracts by specifying the schema.
- Create
- Update
- Delete
import { RestEndpoint } from '@rest-hooks/rest';
const todoCreate = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos',
method: 'POST',
schema: Todo,
});
Example Usage
import { useController } from '@rest-hooks/react';
export default function NewTodoForm() {
const ctrl = useController();
return (
<Form onSubmit={e => ctrl.fetch(todoCreate, new FormData(e.target))}>
<FormField name="title" />
</Form>
);
}
import { RestEndpoint } from '@rest-hooks/rest';
const todoUpdate = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
method: 'PUT',
schema: Todo,
});
Example Usage
import { useController } from '@rest-hooks/react';
export default function UpdateTodoForm({ id }: { id: number }) {
const todo = useSuspense(todoDetail, { id });
const ctrl = useController();
return (
<Form
onSubmit={e => ctrl.fetch(todoUpdate, { id }, new FormData(e.target))}
initialValues={todo}
>
<FormField name="title" />
</Form>
);
}
import { schema, RestEndpoint } from '@rest-hooks/rest';
const todoDelete = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
method: 'DELETE',
schema: new schema.Delete(Todo),
});
Example Usage
import { useController } from '@rest-hooks/react';
export default function TodoWithDelete({ todo }: { todo: Todo }) {
const ctrl = useController();
return (
<div>
{todo.title}
<button onClick={() => ctrl.fetch(todoDelete, { id: todo.id })}>
Delete
</button>
</div>
);
}
Mutations automatically update the normalized cache, resulting in consistent and fresh data.
Schema
Schemas are a declarative definition of how to process responses
- where to expect Entities
- Classes to deserialize fields
import { RestEndpoint } from '@rest-hooks/rest';
const getTodoList = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos',
schema: [Todo],
});
Placing our Entity Todo
in an array, tells Rest Hooks to expect
an array of Todos
.
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.
- Object: maps with known keys
- Array: variably sized arrays
- Union: select from many different types
- Values: maps with any keys - variably sized
- Invalidate: remove an entity
Nesting
Additionally, Entities themselves can specify nested schemas by specifying a static schema member.
- Entity
- Response
import { Entity } from '@rest-hooks/endpoint';
class Todo extends Entity {
readonly id: number = 0;
readonly user: User = User.fromJS({});
readonly title: string = '';
readonly completed: boolean = false;
pk() {
return `${this.id}`;
}
static schema = {
user: User,
};
}
class User extends Entity {
readonly id: number = 0;
readonly username: string = '';
pk() {
return `${this.id}`;
}
}
{
"id": 5,
"user": {
"id": 10,
"username": "bob"
},
"title": "Write some Entities",
"completed": false
}
Data Representations
Additionally, any newable
class that has a toJSON() method, can be used as a schema. This will simply construct the object during denormalization.
This might be useful with representations like bignumber
import { Entity } from '@rest-hooks/endpoint';
class Todo extends Entity {
readonly id: number = 0;
readonly user: User = User.fromJS({});
readonly title: string = '';
readonly completed: boolean = false;
readonly dueDate: Date = new Date(0);
pk() {
return `${this.id}`;
}
static schema = {
user: User,
dueDate: Date,
};
}
Due to the global referential equality guarantee - construction of members only occurs once per update.