Skip to main content

Scalar

Scalar handles entity fields whose values depend on a "lens" selection, such as viewing portfolio-specific data for companies. Multiple components can render the same entity through different lenses simultaneously, each seeing the correct values.

Data is stored in an internal Scalar entity table and joined at denormalize time based on endpoint args. The parent entity stores lens-independent references, so switching lenses never mutates the entity itself.

Usage

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

const PortfolioScalar = new Scalar({
lens: args => args[0]?.portfolio,
key: 'portfolio',
});

class Company extends Entity {
id = '';
price = 0;
pct_equity = 0;
shares = 0;

static schema = {
pct_equity: PortfolioScalar,
shares: PortfolioScalar,
};
}

A single Scalar instance can be shared across multiple Entity classes — the entity context is inferred at normalize time from the parent entity and recorded on the wrapper, so denormalize always resolves to the correct cell. Compound pks remain unique because they are namespaced by entity key (e.g. Company|1|portfolioA vs. Fund|1|portfolioA).

Full entity endpoint

When fetching companies with a portfolio lens, scalar fields are automatically extracted and stored in the Scalar cell table:

import { Endpoint } from '@data-client/rest';

const getCompanies = new Endpoint(
({ portfolio }: { portfolio: string }) =>
fetch(`/companies?portfolio=${portfolio}`).then(r => r.json()),
{ schema: [Company] },
);

Column-only endpoint

Fetch just the lens-dependent columns without refetching entity data. The response is a dictionary keyed by entity pk. Because there is no parent entity in this path, the Scalar must be bound to an Entity class via the entity option:

const CompanyPortfolioScalar = new Scalar({
lens: args => args[0]?.portfolio,
key: 'portfolio',
entity: Company,
});

const getPortfolioColumns = new Endpoint(
({ portfolio }: { portfolio: string }) =>
fetch(`/companies/columns?portfolio=${portfolio}`).then(r => r.json()),
{ schema: new schema.Values(CompanyPortfolioScalar) },
);
// Response: { '1': { pct_equity: 0.5, shares: 32342 }, '2': { ... } }

Column fetches only write Scalar(portfolio) cell entries — they never modify the Company entities.

A bound Scalar can also be used as an Entity.schema field (the binding is just ignored in favor of the inferred parent), so a single instance can serve both endpoints.

Combined usage: portfolio grid

Both endpoints share a single Scalar instance and feed the same grid. getCompanies loads the full row data on first render; getPortfolioColumns fires alongside as a cheaper lens-only refresh — both write to the same Scalar(portfolio) cell table. Switch portfolios in the dropdown to watch % Equity and Shares swap while the Company rows themselves stay stable.

Fixtures
GET /companies?portfolio=A
[{"id":"1","name":"Acme Corp","price":145.2,"pct_equity":0.5,"shares":10000},{"id":"2","name":"Globex","price":89.5,"pct_equity":0.2,"shares":4000},{"id":"3","name":"Initech","price":32.1,"pct_equity":0.1,"shares":2500}]
GET /companies?portfolio=B
[{"id":"1","name":"Acme Corp","price":145.2,"pct_equity":0.3,"shares":6000},{"id":"2","name":"Globex","price":89.5,"pct_equity":0.4,"shares":8000},{"id":"3","name":"Initech","price":32.1,"pct_equity":0.05,"shares":1200}]
GET /companies/columns?portfolio=A
{"1":{"pct_equity":0.5,"shares":10000},"2":{"pct_equity":0.2,"shares":4000},"3":{"pct_equity":0.1,"shares":2500}}
GET /companies/columns?portfolio=B
{"1":{"pct_equity":0.3,"shares":6000},"2":{"pct_equity":0.4,"shares":8000},"3":{"pct_equity":0.05,"shares":1200}}
api/Company
import { Entity, RestEndpoint, Scalar, schema } from '@data-client/rest';

export class Company extends Entity {
  id = '';
  name = '';
  price = 0;
  pct_equity = 0;
  shares = 0;
}

// Bound to Company so both endpoints below can use this same instance.
const PortfolioScalar = new Scalar({
  lens: args => args[0]?.portfolio,
  key: 'portfolio',
  entity: Company,
});
Company.schema = {
  pct_equity: PortfolioScalar,
  shares: PortfolioScalar,
};

export const getCompanies = new RestEndpoint({
  path: '/companies',
  searchParams: {} as { portfolio: string },
  schema: [Company],
});

export const getPortfolioColumns = new RestEndpoint({
  path: '/companies/columns',
  searchParams: {} as { portfolio: string },
  schema: new schema.Values(PortfolioScalar),
});
PortfolioGrid
import { useSuspense, useFetch } from '@data-client/react';
import { getCompanies, getPortfolioColumns } from './api/Company';

function PortfolioGrid() {
  const [portfolio, setPortfolio] = React.useState('A');
  // Full load: Company rows + Scalar cells for the current lens.
  const companies = useSuspense(getCompanies, { portfolio });
  // Cheap lens-only refresh in the background — writes to the same cell table.
  useFetch(getPortfolioColumns, { portfolio });

  return (
    <div>
      <label>
        Portfolio:{' '}
        <select
          value={portfolio}
          onChange={e => setPortfolio(e.currentTarget.value)}
        >
          <option value="A">Portfolio A</option>
          <option value="B">Portfolio B</option>
        </select>
      </label>
      <table style={{ marginTop: 8, width: '100%' }}>
        <thead>
          <tr>
            <th align="left">Name</th>
            <th align="right">Price</th>
            <th align="right">% Equity</th>
            <th align="right">Shares</th>
          </tr>
        </thead>
        <tbody>
          {companies.map(c => (
            <tr key={c.pk()}>
              <td>{c.name}</td>
              <td align="right">${c.price.toFixed(2)}</td>
              <td align="right">{(c.pct_equity * 100).toFixed(1)}%</td>
              <td align="right">{c.shares.toLocaleString()}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
render(<PortfolioGrid />);
🔴 Live Preview
Store

name and price references stay stable across portfolio switches because the Company entity itself never changes — only the Scalar cell selected by the current lens does. In a real app, you'd typically reach for getPortfolioColumns instead of getCompanies after the initial load to avoid refetching lens-independent fields.

Constructor

new Scalar({ lens, key, entity? })

Options

  • lens (args: readonly any[]) => string | undefinedrequired. Extracts the lens value (e.g. portfolio ID) from endpoint args.
  • key stringrequired. A unique name for this scalar type, used to namespace the internal Scalar entity table (e.g. 'portfolio' becomes Scalar(portfolio)).
  • entity Entity classoptional. The Entity class this Scalar attaches to. Only required when the Scalar is used standalone (e.g. inside schema.Values for a column-only endpoint), where there is no parent entity to infer from. When used as a field on Entity.schema, the entity is inferred from the parent and recorded on the wrapper.

How it works

Normalize

When an entity with Scalar schema fields is normalized:

  1. EntityMixin.normalize detects Scalar fields and passes the whole entity object to Scalar.normalize (avoiding the primitive short-circuit in getVisit).
  2. Scalar.normalize discovers its fields from the parent entity's schema, extracts their values, and stores them as a grouped cell via delegate.mergeEntity(this, compoundPk, cellData) (the cell schema is this Scalar instance).
  3. A lens-independent tuple wrapper [entityPk, fieldName, entityKey] replaces each scalar field on the entity (an array, distinguishable from cell data via Array.isArray).

Denormalize

The standard EntityMixin.denormalize loop is completely unchanged:

  1. unvisit(Scalar, wrapper) calls Scalar.denormalize (Scalar is not entity-like).
  2. Scalar.denormalize reads the wrapper, adds the current lens from endpoint args, and builds the compound pk.
  3. It calls unvisit(this, compoundPk) to look up the correct cell, then extracts and returns the specific field value.

This means different components viewing the same entity with different lens args get different scalar values, while sharing the same base entity data.

Normalized storage

entities['Company']['1'] = {
id: '1',
price: 100,
pct_equity: ['1', 'pct_equity', 'Company'],
shares: ['1', 'shares', 'Company'],
}

entities['Scalar(portfolio)']['Company|1|portfolioA'] = {
pct_equity: 0.5,
shares: 32342,
}

entities['Scalar(portfolio)']['Company|1|portfolioB'] = {
pct_equity: 0.3,
shares: 323,
}
  • Entity — defines the base entity that scalar fields attach to
  • Values — used for column-only endpoints (dictionary keyed by entity pk)
  • Union — similar wrapper pattern for polymorphic entities