Pular para o conteúdo principal

Dados relacionais

O Reactive Data Client lida com relacionamentos um-para-um, muitos-para-um e muitos-para-muitos em entities usando Entity.schema

Aninhamento​

Os membros aninhados são extraídos (hoisted) durante a normalização quando Entity.schema é definido. Eles são então reunidos novamente durante a desnormalização

Diagrama
 
Fixtures
GET /posts
[{"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"}}]}]
▶resources/Post
import { Collection, Entity, resource } 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: new Collection([Comment], {
      nestKey: (parent, key) => ({
        postId: parent.id,
      }),
    }),
  };
}

export const PostResource = resource({
  path: '/posts/:id',
  schema: Post,
});
▶PostPage
Resultado
Store▶

Joins no lado do cliente​

Aninhando dados quando o seu endpoint não aninha.

Mesmo que as respostas da rede não aninhem os dados, podemos realizar joins no lado do cliente especificando o relacionamento em Entity.schema

▶resources/User
▶resources/Todo
import { Entity, resource } from '@data-client/rest';
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,
});
▶TodoJoined
Resultado
Store▶

Joins baseados em chave​

Para cenários mais complexos em que as entities relacionadas são buscadas separadamente, use Entity.process() para criar uma chave de referência que aponte para outra Entity. Isso é útil quando:

  • Os dados relacionados vêm de endpoints de API diferentes
  • Você quer evitar buscar dados aninhados em excesso
  • O relacionamento é opcional ou varia conforme o contexto
import { Entity, resource } from '@data-client/rest';

class Stats extends Entity {
product_id = '';
volume = 0;
price = 0;

pk() {
return this.product_id;
}

static key = 'Stats';
}

class Currency extends Entity {
id = '';
name = '';
// Default value allows Currency to exist without Stats loaded
stats = Stats.fromJS();

pk() {
return this.id;
}

static key = 'Currency';

// Create a reference key that links to Stats entity
static process(input: any, parent: any, key: string, args: any[]) {
// The stats field becomes a reference to Stats with pk `${id}-USD`
return { ...input, stats: `${input.id}-USD` };
}

static schema = {
// Stats will be looked up by the key from process()
stats: Stats,
};
}

Quando CurrencyResource.getList e StatsResource.getList são ambos buscados, o campo stats será resolvido automaticamente para a entity Stats correspondente.

Exemplo de preço de criptomoedas​

Aqui queremos ordenar Currencies pelo volume de negociação. No entanto, o volume de negociação só está disponível na Entity Stats. Embora o fetch de CurrencyResource.getList não inclua Stats na resposta, podemos adicionalmente chamar StatsResource.getList, adicionando-o ao Currency's Entity.schema, o que permite incluir Stats na Entity Currency e, assim, ordenar com:

entries.sort((a, b) => {
return b?.stats?.volume_usd - a?.stats?.volume_usd;
});

Explore o exemplo coin-app

More Demos

Buscas reversas​

Aninhando dados quando o seu endpoint não aninha (parte 2).

Mesmo que uma resposta aninhe os dados em apenas uma direção, o Reactive Data Client consegue lidar com relacionamentos reversos sobrescrevendo Entity.process. Além disso, pode ser necessário sobrescrever Entity.merge para garantir o merge profundo desses campos esperados.

Isso permite percorrer o relacionamento após processar apenas uma requisição de fetch, em vez de precisar buscar toda vez que você quiser acessar uma visão diferente.

Fixtures
GET /posts
[{"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"}}]}]
▶resources/Post
import { Entity, resource, type Schema } 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,
});
▶UserPage
▶PostPage
▶Navigation
Resultado
Store▶

Dependências circulares​

Como imports circulares e definições circulares de classes não são permitidos, às vezes será necessário definir o schema depois da definição das Entities.

resources/Post
import { Collection, 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],
};
resources/User
import { Collection, 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.fromEpochMilliseconds(0);

static schema: Record<string, Schema | Date> = {
createdAt: Temporal.Instant.from,
};
}
dica

Para relacionamentos bidirecionais que não precisam de desnormalização antecipada, Lazy adia a resolução e permite resolvê-la sob demanda por meio de useQuery, evitando recursão profunda e melhorando o isolamento da memoização.