Skip to main content

Summary List Endpoints

Sometimes you have a list endpoint that includes only a subset of fields.

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

Fixtures
GET /article
[{"id":"1","title":"first"},{"id":"2","title":"second"}]
GET /article/1
{"id":"1","title":"first","content":"long","createdAt":"2011-10-05T14:48:00.000Z"}
GET /article/2
{"id":"2","title":"second","content":"short","createdAt":"2011-10-05T14:48:00.000Z"}
api/Article.ts
class ArticleSummary extends Entity {
readonly id: string = '';
readonly title: string = '';
pk() {
return this.id;
}
// this ensures `Article` maps to the same entity
static get key() {
return 'Article';
}
}
class Article extends ArticleSummary {
readonly content: string = '';
readonly createdAt: Date = new Date(0);
static schema = {
createdAt: Date,
};
static validate(processedEntity) {
return (
validateRequired(processedEntity, this.defaults) ||
super.validate(processedEntity)
);
}
}
const BaseArticleResource = createResource({
path: '/article/:id',
schema: Article,
});
const ArticleResource = {
...BaseArticleResource,
getList: BaseArticleResource.getList.extend({ schema: [ArticleSummary] }),
};
ArticleDetail.tsx
function ArticleDetail({ id, onHome }: { id: string; onHome: () => void }) {
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>
{Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(
article.createdAt,
)}
</time>
</div>
</div>
</div>
);
}
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
Loading...
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 {
readonly id: string = '';
readonly title: string = '';
readonly content: string = '';
readonly createdAt: Date = new Date(0);

static schema = {
createdAt: Date,
};

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

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

static schema = {
...super.schema,
meta: ArticleMeta,
}

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

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

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

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