Skip to content

Latest commit

 

History

History
263 lines (217 loc) · 5.88 KB

File metadata and controls

263 lines (217 loc) · 5.88 KB
title sidebar_label description
Thinking in Schemas
Schema
Declarative TypeScript data definitions. Mutable dynamic data applications without state management code.

import LanguageTabs from '@site/src/components/LanguageTabs'; import SchemaTable from '../../core/shared/_schema_table.mdx';

Consider a typical blog post. The API response for a single post might look something like this:

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "createdAt": "2013-05-29T00:00:00-04:00",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    },
    {
      "id": "544",
      "createdAt": "2013-05-30T00:00:00-04:00",
      "commenter": {
        "id": "1",
        "name": "Paul"
      }
    }
  ]
}

Declarative definitions

We have two nested entity types within our article: users and comments. Using various schema, we can normalize all three entity types down:

import { schema, Entity } from '@data-client/endpoint';
import { Temporal } from '@js-temporal/polyfill';

class User extends Entity {
  id = '';
  name = '';
}

class Comment extends Entity {
  id = '';
  createdAt = Temporal.Instant.fromEpochMilliseconds(0);
  commenter = User.fromJS();

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

class Article extends Entity {
  id = '';
  title = '';
  author = User.fromJS();
  comments: Comment[] = [];

  static schema = {
    author: User,
    comments: [Comment],
  };
}
import { schema, Entity } from '@data-client/endpoint';
import { Temporal } from '@js-temporal/polyfill';

class User extends Entity { }

class Comment extends Entity {
  static schema = {
    commenter: User,
    createdAt: Temporal.Instant.from,
  };
}

class Article extends Entity {
  static schema = {
    author: User,
    comments: [Comment],
  };
}

Normalize

import { normalize } from '@data-client/normalizr';

const args = [{ id: '123' }];
const normalizedData = normalize(Article, originalData, args);

Now, normalizedData will create a single serializable source of truth for all entities:

{
  result: "123",
  entities: {
    articles: {
      "123": {
        id: "123",
        author: "1",
        title: "My awesome blog post",
        comments: [ "324", "544" ]
      }
    },
    users: {
      "1": { "id": "1", "name": "Paul" },
      "2": { "id": "2", "name": "Nicole" }
    },
    comments: {
      "324": {
        id: "324",
        createdAt: "2013-05-29T00:00:00-04:00",
        commenter: "2"
      },
      "544": {
        id: "544",
        createdAt: "2013-05-30T00:00:00-04:00",
        commenter: "1"
      }
    }
  },
  // contents excluded for brevity
  indexes,
  entitiesMeta,
}

Denormalize

import { denormalize } from '@data-client/normalizr';

const denormalizedData = denormalize(
  Article,
  normalizedData.result,
  normalizedData.entities,
  args,
);

Now, denormalizedData will instantiate the classes, ensuring all instances of the same member (like Paul) are referentially equal:

Article {
  id: '123',
  title: 'My awesome blog post',
  author: User { id: '1', name: 'Paul' },
  comments: [
    Comment {
      id: '324',
      createdAt: Instant [Temporal.Instant] {},
      commenter: [User { id: '2', name: 'Nicole' }]
    },
    Comment {
      id: '544',
      createdAt: Instant [Temporal.Instant] {},
      commenter: [User { id: '1', name: 'Paul' }]
    }
  ]
}

MemoCache

MemoCache is a singleton that can be used to maintain referential equality between calls as well as potentially improved performance by 2000%. Its methods are memoized.

memo.denormalize

import { MemoCache } from '@data-client/normalizr';

// you can construct a new memo anytime you want to reset the cache
const memo = new MemoCache();

const { data, paths } = memo.denormalize(
  Article,
  normalizedData.result,
  normalizedData.entities,
  args,
);
const { data: data2 } = memo.denormalize(
  Article,
  normalizedData.result,
  normalizedData.entities,
  args,
);

// referential equality maintained between calls
assert(data === data2);

memo.denormalize() is just like denormalize() above but includes paths as part of the return value. paths is an Array of paths of all entities included in the result.

memo.query

memo.query() allows denormalizing Queryable based on args alone, rather than a normalized input.

const data = memo.query(
  Article,
  args,
  normalizedData,
);

Queryable

Queryable Schemas allow store access without an endpoint. They achieve this using the queryKey method that produces the results normally stored in the endpoint cache.

This enables their use in these additional cases:

Querables include Entity, All, Collection, Query, and Union.

interface Queryable {
  queryKey(
    args: readonly any[],
    queryKey: (...args: any) => any,
    getEntity: GetEntity,
    getIndex: GetIndex,
    // `{}` means non-void
  ): {};
}

Schema Overview