Skip to main content

Partial Entities

Sometimes you have a list endpoint whose entities only include a subset of fields needed to summarize.

ArticleSummary
{
"id": "1",
"title": "first"
}
Article
{
"id": "1",
"title": "first",
"content": "Imagine there was much more here.",
"createdAt": "2011-10-05T14:48:00.000Z"
}

In this case we can override Entity.validate() using validateRequired() to ensure we have the full and complete response when needed (detail views), while keeping our state DRY and normalized to ensure data integrity.

import { validateRequired } from '@data-client/rest';
import { Entity, createResource, schema } from '@data-client/rest';

export class ArticleSummary extends Entity {
  id = '';
  title = '';

  pk() {
    return this.id;
  }
  // this ensures `Article` maps to the same entity
  static key = 'Article';

  static schema = {
    createdAt: Temporal.Instant.from,
  };
}

export class Article extends ArticleSummary {
  content = '';
  createdAt = Temporal.Instant.fromEpochSeconds(0);

  static validate(processedEntity) {
    return validateRequired(processedEntity, this.defaults);
  }
}

export const ArticleResource = createResource({
  path: '/article/:id',
  schema: Article,
}).extend({
  getList: {
    schema: new schema.Collection([ArticleSummary]),
  },
});
import { ArticleResource } from './api/Article';

function ArticleDetail({ id, onHome }: Props) {
  const article = useSuspense(ArticleResource.get, { id });
  return (
    <div>
      <h4>
        <a onClick={onHome} style={{ cursor: 'pointer' }}>
          &lt;
        </a>{' '}
        {article.title}
      </h4>
      <div>
        <p>{article.content}</p>
        <div>
          Created:{' '}
          <time>
            {DateTimeFormat('en-US', { dateStyle: 'medium' }).format(
              article.createdAt,
            )}
          </time>
        </div>
      </div>
    </div>
  );
}
interface Props {
  id: string;
  onHome: () => void;
}
function ArticleList() {
  const [route, setRoute] = React.useState('');
  const articles = useSuspense(ArticleResource.getList);
  if (!route) {
    return (
      <div>
        {articles.map(article => (
          <div
            key={article.pk()}
            onClick={() => setRoute(article.id)}
            style={{ cursor: 'pointer', textDecoration: 'underline' }}
          >
            Click me: {article.title}
          </div>
        ))}
      </div>
    );
  }
  return <ArticleDetail id={route} onHome={() => setRoute('')} />;
}

render(<ArticleList />);
🔴 Live Preview
Store

Detail data in nested entity

It's often better to move expensive data into another entity to simplify conditional logic.

api/Article.ts
class ArticleSummary extends Entity {
id = '';
title = '';
content = '';
createdAt = Temporal.Instant.fromEpochSeconds(0);

static schema = {
createdAt: Temporal.Instant.from,
meta: ArticleMeta,
};

pk() {
return this.id;
}
// this ensures `Article` maps to the same entity
static key = 'Article';
}

class Article extends ArticleSummary {
meta = ArticleMeta.fromJS();

static validate(processedEntity) {
return validateRequired(processedEntity, this.defaults);
}
}

class ArticleMeta extends Entity {
viewCount = 0;
likeCount = 0;
relatedArticles: ArticleSummary[] = [];

static schema = {
relatedArticles: [ArticleSummary],
};
}

const ArticleResource = createResource({
path: '/article/:id',
schema: Article,
}).extend({
getList: { schema: new schema.Collection([ArticleSummary]) },
});