‹ Build a Dashboard Lesson 5 of 16
Contents Lesson 5 of 16

4 min read · professional

The panel contract

Four panel types today, more later. What makes that survivable is a contract every panel honours, written once.

The interface

type PanelProps<C> = {
  config: C;                       // already validated by type
  data: PanelData;                 // given, never fetched
  onSelect(symbol: string): void;  // the only way out
  onConfigChange(next: C): void;   // the only way to persist
};

Four rules, and each one exists because of a specific failure:

Data comes in. A panel that fetches is a panel that cannot be tested without a network, cannot be rendered twice cheaply, and will duplicate a request. This is the data-layer rule from unit 1, expressed as a signature.

Config comes in validated. The panel never parses its own config. One validator per type, at the boundary, so a panel body contains no defensive parsing at all.

Communication goes out through callbacks. A panel never reaches into a global store to change something. It says what happened; the container decides what that means.

Panels do not know about each other. No panel imports another. The moment one does, you have a pair rather than a system, and the third panel will need a different pair.

Declare what data you need

A panel cannot fetch, so it must be able to say what it wants:

const WatchlistPanel = {
  dataNeeds: (config) => ({ quotes: config.symbols }),
  render: (props) => { /* ... */ },
};

The container collects dataNeeds from every visible panel, unions the symbol sets, and hands that one list to the data layer. Two panels wanting AAPL produce one entry. This is where the deduplication from unit 1 actually happens — the store can only dedupe what it is asked for in one go.

It also gives you the refresh budget for free, which is unit 4: the union of dataNeeds across the layout, priced by the table from unit 1, is exactly what one refresh costs.

The panel body is small, and that is the test

If a panel body is more than a screenful, something that belongs in the container has leaked into it. The usual culprits: fetching, parsing, cross-panel coordination, or persistence.

A good panel is a pure function of its props. You can render it in a test with a literal fixture and no mocks, and that is the property to check.

Where generated code goes wrong here

Ask an assistant for "a watchlist panel" and you will get one that fetches, parses, holds its own loading state and writes to localStorage — because that is what a standalone component looks like in every example it has read.

So do not ask for a panel. Give it the contract and ask for an implementation of it. The difference in what comes back is the largest single return on the planning discipline from course 1.

Try it now

Write the contract, then convert your existing watchlist into a panel that satisfies it. Whatever you have to delete from it is the code that was never about watchlists — and that deletion is the whole lesson.