Overdue Orders: a Derived Grid That Maintains Itself
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.