Relational data
Reactive Data Client handles one-to-one, many-to-one and many-to-many relationships on entities using Entity.schema
Nesting
Nested members are hoisted during normalization when Entity.schema is defined. They are then rejoined during denormalization
Diagram
[{"id":"1","title":"My first post!","author":{"id":"123","name":"Paul"},"comments":[{"id":"249","content":"Nice post!","commenter":{"id":"245","name":"Jane"}},{"id":"250","content":"Thanks!","commenter":{"id":"123","name":"Paul"}}]},{"id":"2","title":"This other post","author":{"id":"123","name":"Paul"},"comments":[{"id":"251","content":"Your other post was nicer","commenter":{"id":"245","name":"Jane"}},{"id":"252","content":"I am a spammer!","commenter":{"id":"246","name":"Spambot5000"}}]}]
import { Entity } from '@data-client/rest'; export class User extends Entity { id = ''; name = ''; } export class Comment extends Entity { id = ''; content = ''; commenter = User.fromJS(); static schema = { commenter: User, }; } export class Post extends Entity { id = ''; title = ''; author = User.fromJS(); comments: Comment[] = []; static schema = { author: User, comments: [Comment], }; } export const PostResource = resource({ path: '/posts/:id', schema: Post, });
Client side joins
Nesting data when your endpoint doesn't.
Even if the network responses don't nest data, we can perform client-side joins by specifying the relationship in Entity.schema
import { User } from './User'; export class Todo extends Entity { id = 0; userId = 0; user? = User.fromJS(); title = ''; completed = false; static schema = { user: User, }; static process(todo) { return { ...todo, user: todo.userId }; } } export const TodoResource = resource({ urlPrefix: 'https://jsonplaceholder.typicode.com', path: '/todos/:id', schema: Todo, });
Crypto price example
Here we want to sort Currencies
by their trade volume. However, trade volume is only available in the Stats
Entity. Even though CurrencyResource.getList
fetch does not include Stats
in the response, we can additionally
call StatsResource.getList
, while adding it to our Currency's
Entity.schema - enabling
Stats
inclusion in our Currency
Entity, which enables sorting with:
entries.sort((a, b) => {
return b?.stats?.volume_usd - a?.stats?.volume_usd;
});
Reverse lookups
Nesting data when your endpoint doesn't (part 2).
Even though a response may only nest in one direction, Reactive Data Client can handle reverse relationships by overriding Entity.process. Additionally, Entity.merge may need overriding to ensure deep merging of those expected fields.
This allows you to traverse the relationship after processing only one fetch request, rather than having to fetch each time you want access to a different view.
[{"id":"1","title":"My first post!","author":{"id":"123","name":"Paul"},"comments":[{"id":"249","content":"Nice post!","commenter":{"id":"245","name":"Jane"}},{"id":"250","content":"Thanks!","commenter":{"id":"123","name":"Paul"}}]},{"id":"2","title":"This other post","author":{"id":"123","name":"Paul"},"comments":[{"id":"251","content":"Your other post was nicer","commenter":{"id":"245","name":"Jane"}},{"id":"252","content":"I am a spammer!","commenter":{"id":"246","name":"Spambot5000"}}]}]
import { Entity } from '@data-client/rest'; export class User extends Entity { id = ''; name = ''; posts: Post[] = []; comments: Comment[] = []; static merge(existing, incoming) { return { ...existing, ...incoming, posts: [...(existing.posts || []), ...(incoming.posts || [])], comments: [ ...(existing.comments || []), ...(incoming.comments || []), ], }; } static process(value, parent, key) { switch (key) { case 'author': return { ...value, posts: [parent.id] }; case 'commenter': return { ...value, comments: [parent.id] }; default: return { ...value }; } } } export class Comment extends Entity { id = ''; content = ''; commenter = User.fromJS(); post = Post.fromJS(); static schema: Record<string, Schema> = { commenter: User, }; static process(value, parent, key) { return { ...value, post: parent.id }; } } export class Post extends Entity { id = ''; title = ''; author = User.fromJS(); comments: Comment[] = []; static schema = { author: User, comments: [Comment], }; } // with cirucular dependencies we must set schema after they are all defined User.schema = { posts: [Post], comments: [Comment], }; Comment.schema = { ...Comment.schema, post: Post, }; export const PostResource = resource({ path: '/posts/:id', schema: Post, dataExpiryLength: Infinity, }); export const UserResource = resource({ path: '/users/:id', schema: User, });
Circular dependencies
Because circular imports and circular class definitions are not allowed, sometimes it will be necessary to define the schema after the Entities definition.
import { Entity } from '@data-client/rest';
import { User } from './User';
export class Post extends Entity {
id = '';
title = '';
author = User.fromJS();
static schema = {
author: User,
};
}
// both User and Post are now defined, so it's okay to refer to both of them
User.schema = {
// ensure we keep the 'createdAt' member
...User.schema,
posts: [Post],
};
import { Entity } from '@data-client/rest';
import type { Post } from './Post';
// we can only import the type else we break javascript imports
// thus we change the schema of UserResource above
export class User extends Entity {
id = '';
name = '';
posts: Post[] = [];
createdAt = Temporal.Instant.fromEpochSeconds(0);
static schema: Record<string, Schema | Date> = {
createdAt: Temporal.Instant.from,
};
}