Lattice Grid Buy a licence

blog

Overdue Orders: a Derived Grid That Maintains Itself

Building…
Loading a live grid…

The support lead’s morning starts the same way: filter the orders table to unpaid, sort by due date, and scroll for anything that has slipped past it, or run the saved query that does the same thing against the database. It is a list rebuilt from scratch each time, and it is only ever as current as the moment it was built. By Wednesday three more orders have gone overdue and nobody is watching for them until the next rebuild. An exception report that lives in the data grid itself never needs that rebuild.

A derived grid turns that query into a second grid wired to the first. Point it at the orders grid, give it a where clause instead of a groupBy, status is not Paid and the due date is before today, and it reads the matching rows themselves rather than reducing them to a total. That is what an exceptions list is: the rows that fail a test, not a number that summarises them.

The setting that matters here is follow: 'all' rather than the filtered default. The orders table is also where the team looks something up: one customer’s history, one region, one order number. Each time it is filtered for that, a derived grid reading follow: 'filtered' would narrow with it, and the overdue list would quietly drop every order outside whatever search is currently on screen. follow: 'all' keeps it reading every row in the orders grid regardless of how the table itself is filtered at that moment, so looking something up and keeping watch do not compete for the same filter.

The overdue list no longer needs its own query, its own daily rebuild, or a reminder to rerun it before the morning review. Mark one of the orders below Paid, or add an eleventh past its due date, and the list changes with it, with nothing rerun by hand.

Plain JavaScript

import { createGrid } from '@toclocoinc/lattice-grid';

// Pinned so the figures below stay fixed; in your app use today's date:
// const today = new Date().toISOString().slice(0, 10);
const today = '2026-10-01';

const orders = [
  { id: 1,  customer: 'Acme Corp',      status: 'Open', dueDate: '2026-09-15', amount: 540 },
  { id: 2,  customer: 'Acme Corp',      status: 'Paid', dueDate: '2026-09-10', amount: 320 },
  { id: 3,  customer: 'Brightline',     status: 'Open', dueDate: '2026-09-28', amount: 275 },
  { id: 4,  customer: 'Brightline',     status: 'Paid', dueDate: '2026-09-20', amount: 410 },
  { id: 5,  customer: 'Cobalt Labs',    status: 'Open', dueDate: '2026-10-05', amount: 690 },
  { id: 6,  customer: 'Cobalt Labs',    status: 'Open', dueDate: '2026-08-30', amount: 150 },
  { id: 7,  customer: 'Dune Systems',   status: 'Paid', dueDate: '2026-09-05', amount: 980 },
  { id: 8,  customer: 'Dune Systems',   status: 'Open', dueDate: '2026-09-18', amount: 225 },
  { id: 9,  customer: 'Ferro Retail',   status: 'Open', dueDate: '2026-09-25', amount: 860 },
  { id: 10, customer: 'Ferro Retail',   status: 'Paid', dueDate: '2026-09-29', amount: 310 },
  { id: 11, customer: 'Glacier Inc',    status: 'Open', dueDate: '2026-10-08', amount: 440 },
  { id: 12, customer: 'Glacier Inc',    status: 'Open', dueDate: '2026-09-02', amount: 670 },
  { id: 13, customer: 'Harbor Goods',   status: 'Paid', dueDate: '2026-08-20', amount: 510 },
  { id: 14, customer: 'Harbor Goods',   status: 'Open', dueDate: '2026-09-12', amount: 390 },
  { id: 15, customer: 'Ionix Partners', status: 'Open', dueDate: '2026-09-22', amount: 720 },
  { id: 16, customer: 'Ionix Partners', status: 'Paid', dueDate: '2026-09-27', amount: 260 },
  { id: 17, customer: 'Juno Freight',   status: 'Open', dueDate: '2026-09-08', amount: 580 },
  { id: 18, customer: 'Juno Freight',   status: 'Open', dueDate: '2026-10-03', amount: 330 },
  { id: 19, customer: 'Kestrel Media',  status: 'Paid', dueDate: '2026-09-14', amount: 450 },
  { id: 20, customer: 'Kestrel Media',  status: 'Open', dueDate: '2026-09-19', amount: 610 },
];

const book = createGrid(document.querySelector('#orders'), {
  rowKey: 'id',
  columns: [
    { field: 'id',       title: 'Order' },
    { field: 'customer', title: 'Customer', filter: { type: 'set' } },
    { field: 'status',   title: 'Status',   filter: { type: 'set' } },
    { field: 'dueDate',  title: 'Due',      type: 'dateString' },
    { field: 'amount',   title: 'Amount',   type: 'number' },
  ],
  rows: orders,
});

// A second grid, reading every row of the first regardless of its filters.
const overdue = createGrid(document.querySelector('#overdue'), {
  source: {
    mode: 'derived',
    from: book,                                            // the grid to read
    follow: 'all',                                          // read every row, filtered or not
    where: (r) => r.status !== 'Paid' && r.dueDate < today,
    sort: [{ col: 'dueDate', dir: 'asc' }],
  },
  columns: [
    { field: 'id',       title: 'Order' },
    { field: 'customer', title: 'Customer' },
    { field: 'dueDate',  title: 'Due' },
    { field: 'amount',   title: 'Amount', type: 'number' },
  ],
});

Filter the orders grid down to a single customer to look up their history, and overdue keeps showing all ten overdue orders across every customer, because follow: 'all' never narrowed with the table in the first place.

React

import React from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';

// Build the component once, at module scope, not on every render.
const LatticeGrid = createLatticeGrid({ React, createGrid });

// Pinned so the figures below stay fixed; in your app use today's date:
// const TODAY = new Date().toISOString().slice(0, 10);
const TODAY = '2026-10-01';

function OverdueOrders({ orders }) {
  const [book, setBook] = React.useState(null);

  const orderColumns = [
    { field: 'id',       title: 'Order' },
    { field: 'customer', title: 'Customer', filter: { type: 'set' } },
    { field: 'status',   title: 'Status',   filter: { type: 'set' } },
    { field: 'dueDate',  title: 'Due',      type: 'dateString' },
    { field: 'amount',   title: 'Amount',   type: 'number' },
  ];

  const overdueColumns = [
    { field: 'id',       title: 'Order' },
    { field: 'customer', title: 'Customer' },
    { field: 'dueDate',  title: 'Due' },
    { field: 'amount',   title: 'Amount', type: 'number' },
  ];

  return (
    <>
      <LatticeGrid
        rowKey="id"
        columns={orderColumns}
        rows={orders}
        onGridReady={setBook}
        style={{ height: 320 }}
      />
      {book && (
        <LatticeGrid
          columns={overdueColumns}
          source={{
            mode: 'derived',
            from: book,
            follow: 'all',
            where: (r) => r.status !== 'Paid' && r.dueDate < TODAY,
            sort: [{ col: 'dueDate', dir: 'asc' }],
          }}
          style={{ height: 240 }}
        />
      )}
    </>
  );
}

onGridReady hands back the live orders grid as soon as it mounts, which is what the overdue panel’s from needs; the panel itself only renders once that instance exists. Filtering the orders grid to look up one customer leaves the overdue panel exactly as full as before, the same as in the plain example, because both read follow: 'all' on the grid’s public API, not a copy of its rows taken at render time.

The options used

Option What it does
mode: 'derived' This grid’s rows are computed from another grid’s, not loaded from an array or a server.
from The grid to read.
follow: 'all' Read every row of the source, not just the ones its own filters currently leave.
where A row predicate, applied before any grouping. With no groupBy, the matching rows pass through as themselves, which is how an exceptions list is built.
sort How to order the derived rows before any limit is applied.

The exception report it produced

Running the plain JavaScript example above with createHeadlessGrid from the published package, with today pinned to 1 October 2026, and printing overdue’s rows, gives:

Order Customer Due Amount
6 Cobalt Labs 2026-08-30 150
12 Glacier Inc 2026-09-02 670
17 Juno Freight 2026-09-08 580
14 Harbor Goods 2026-09-12 390
1 Acme Corp 2026-09-15 540
8 Dune Systems 2026-09-18 225
20 Kestrel Media 2026-09-19 610
15 Ionix Partners 2026-09-22 720
9 Ferro Retail 2026-09-25 860
3 Brightline 2026-09-28 275

Ten orders, 5,020 in total, out of twenty on the book: the seven marked Paid are excluded regardless of their due date, and the three still Open but not yet due, Cobalt Labs, Glacier Inc and Juno Freight’s later orders, are excluded too. Filtering the orders grid to a single customer and re-reading overdue still returns all ten rows, which is follow: 'all' doing exactly what it says.

The derived and chained grids guide covers joins, column profiles, chaining a derived grid from a derived grid, and cross-filtering back into the source in full. For the first post in this series, building a revenue by region summary the same way, see Revenue by Region: a Derived Grid, Not a Second Query.

Read next

All posts RSS